Skip to content

Commit f7a987c

Browse files
Dillon Nysdnys1
authored andcommitted
fix(sigv4): Do not chunk GET/HEAD on Web
Since chunked requests require a body (even for empty payloads), they cannot be sent on Web platforms since the browser APIs (XMLHttpRequest/fetch) disallow bodies for these methods. commit-id:8ff66847
1 parent 8dded62 commit f7a987c

File tree

2 files changed

+83
-4
lines changed

2 files changed

+83
-4
lines changed

packages/aws_signature_v4/lib/src/configuration/services/s3.dart

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,20 @@ class S3ServiceConfiguration extends BaseServiceConfiguration {
104104
return decodedLength + metadataLength;
105105
}
106106

107+
/// Whether [request] should be chunked, given the environment and whether
108+
/// chunking was requested.
109+
bool _shouldChunk(AWSBaseHttpRequest request) {
110+
var isChunkableMethod = true;
111+
if (zIsWeb) {
112+
// Browser APIs (XMLHttpRequest, fetch) disallow sending bodies for these
113+
// methods and, thus, chunked requests are not possible and we should not
114+
// try.
115+
isChunkableMethod = request.method != AWSHttpMethod.get &&
116+
request.method != AWSHttpMethod.head;
117+
}
118+
return chunked && isChunkableMethod;
119+
}
120+
107121
@override
108122
void applySigned(
109123
Map<String, String> headers, {
@@ -122,7 +136,7 @@ class S3ServiceConfiguration extends BaseServiceConfiguration {
122136
contentLength: contentLength,
123137
);
124138

125-
if (chunked) {
139+
if (_shouldChunk(request)) {
126140
// Raw size of the data to be sent, before compression and without metadata.
127141
headers[AWSHeaders.decodedContentLength] = contentLength.toString();
128142

@@ -148,7 +162,7 @@ class S3ServiceConfiguration extends BaseServiceConfiguration {
148162
required bool presignedUrl,
149163
}) async {
150164
// Only unchunked, signed requests are hashed as other services would be.
151-
if (signPayload && !chunked) {
165+
if (signPayload && !_shouldChunk(request)) {
152166
return super.hashPayload(request, presignedUrl: presignedUrl);
153167
}
154168
return hashPayloadSync(request, presignedUrl: presignedUrl);
@@ -162,7 +176,7 @@ class S3ServiceConfiguration extends BaseServiceConfiguration {
162176
if (presignedUrl || !signPayload) {
163177
return unsignedPayloadHash;
164178
}
165-
if (chunked) {
179+
if (_shouldChunk(request)) {
166180
return chunkedPayloadSeedHash;
167181
}
168182
return super.hashPayloadSync(request, presignedUrl: presignedUrl);
@@ -177,7 +191,9 @@ class S3ServiceConfiguration extends BaseServiceConfiguration {
177191
required AWSCredentialScope credentialScope,
178192
required CanonicalRequest canonicalRequest,
179193
}) async* {
180-
if (canonicalRequest.presignedUrl || !signPayload || !chunked) {
194+
if (canonicalRequest.presignedUrl ||
195+
!signPayload ||
196+
!_shouldChunk(canonicalRequest.request)) {
181197
yield* super.signBody(
182198
algorithm: algorithm,
183199
contentLength: contentLength,

packages/aws_signature_v4/test/s3/s3_service_configuration_test.dart

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import 'package:aws_common/aws_common.dart';
1616
import 'package:aws_signature_v4/aws_signature_v4.dart';
1717
import 'package:test/test.dart';
1818

19+
import '../common.dart';
20+
1921
void main() {
2022
const bodyHash =
2123
'44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072';
@@ -84,5 +86,66 @@ void main() {
8486
throwsA(isA<AssertionError>()),
8587
);
8688
});
89+
90+
group(
91+
'GET/HEAD do not contain body on Web',
92+
() {
93+
final request = AWSHttpRequest.get(Uri.parse('https://example.com'));
94+
const signer = AWSSigV4Signer(
95+
credentialsProvider: AWSCredentialsProvider(dummyCredentials),
96+
);
97+
final serviceConfigurations = [
98+
S3ServiceConfiguration(chunked: true, signPayload: true),
99+
S3ServiceConfiguration(chunked: false, signPayload: true),
100+
S3ServiceConfiguration(chunked: false, signPayload: false),
101+
];
102+
103+
void expectNoContentLength(Iterable<String> signedHeaders) {
104+
expect(
105+
CaseInsensitiveSet(signedHeaders),
106+
isNot(contains(AWSHeaders.contentLength)),
107+
);
108+
}
109+
110+
group('sign', () {
111+
for (final serviceConfiguration in serviceConfigurations) {
112+
test(
113+
'chunked=${serviceConfiguration.chunked}, '
114+
'signPayload=${serviceConfiguration.signPayload}',
115+
() async {
116+
final signedRequest = await signer.sign(
117+
request,
118+
credentialScope: dummyCredentialScope,
119+
serviceConfiguration: serviceConfiguration,
120+
);
121+
expectNoContentLength(
122+
signedRequest.canonicalRequest.signedHeaders,
123+
);
124+
},
125+
);
126+
}
127+
});
128+
129+
group('signSync', () {
130+
for (final serviceConfiguration in serviceConfigurations) {
131+
test(
132+
'chunked=${serviceConfiguration.chunked}, '
133+
'signPayload=${serviceConfiguration.signPayload}',
134+
() {
135+
final signedRequest = signer.signSync(
136+
request,
137+
credentialScope: dummyCredentialScope,
138+
serviceConfiguration: serviceConfiguration,
139+
);
140+
expectNoContentLength(
141+
signedRequest.canonicalRequest.signedHeaders,
142+
);
143+
},
144+
);
145+
}
146+
});
147+
},
148+
testOn: 'browser',
149+
);
87150
});
88151
}

0 commit comments

Comments
 (0)