Skip to content

Commit 060eff2

Browse files
Martin Kamleithnermicimize
authored andcommitted
fix(client): Throw a ClientException on non-json responses, to be
consistent with other malformed responses. Fix tests to also expect a ClientException in theses cases. Fixes zino-hofmann#552.
1 parent b22d911 commit 060eff2

File tree

2 files changed

+87
-68
lines changed

2 files changed

+87
-68
lines changed

packages/graphql/lib/src/link/http/link_http.dart

Lines changed: 84 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ class HttpLink extends Link {
2121
HttpLink({
2222
@required String uri,
2323
bool includeExtensions,
24-
bool useGETForQueries = false,
2524

2625
/// pass on customized httpClient, especially handy for mocking and testing
2726
Client httpClient,
@@ -49,7 +48,6 @@ class HttpLink extends Link {
4948
final HttpConfig linkConfig = HttpConfig(
5049
http: HttpQueryOptions(
5150
includeExtensions: includeExtensions,
52-
useGETForQueries: useGETForQueries,
5351
),
5452
options: fetchOptions,
5553
credentials: credentials,
@@ -60,42 +58,44 @@ class HttpLink extends Link {
6058
HttpConfig contextConfig;
6159

6260
if (context != null) {
63-
// TODO: for backwards-compatability fallback to overall context for http options
64-
dynamic httpContext = context['http'] ?? context ?? {};
6561
// TODO: refactor context to use a [HttpConfig] object to avoid dynamic types
6662
contextConfig = HttpConfig(
6763
http: HttpQueryOptions(
68-
includeQuery: httpContext['includeQuery'] as bool,
69-
includeExtensions: httpContext['includeExtensions'] as bool,
70-
useGETForQueries: httpContext['useGETForQueries'] as bool,
64+
includeExtensions: context['includeExtensions'] as bool,
7165
),
7266
options: context['fetchOptions'] as Map<String, dynamic>,
7367
credentials: context['credentials'] as Map<String, dynamic>,
7468
headers: context['headers'] as Map<String, String>,
7569
);
7670
}
7771

78-
final HttpConfig config = _mergeHttpConfigs(
72+
final HttpHeadersAndBody httpHeadersAndBody =
73+
_selectHttpOptionsAndBody(
74+
operation,
7975
fallbackHttpConfig,
8076
linkConfig,
8177
contextConfig,
8278
);
8379

80+
final Map<String, String> httpHeaders = httpHeadersAndBody.headers;
81+
8482
StreamController<FetchResult> controller;
8583

8684
Future<void> onListen() async {
8785
StreamedResponse response;
8886

8987
try {
9088
// httpOptionsAndBody.body as String
91-
final BaseRequest request = await _prepareRequest(parsedUri, operation, config);
89+
final BaseRequest request = await _prepareRequest(
90+
parsedUri, httpHeadersAndBody.body, httpHeaders);
9291

9392
response = await fetcher.send(request);
9493

9594
operation.setContext(<String, StreamedResponse>{
9695
'response': response,
9796
});
98-
final FetchResult parsedResponse = await _parseResponse(response);
97+
final FetchResult parsedResponse =
98+
await _parseResponse(response);
9999

100100
controller.add(parsedResponse);
101101
} catch (failure) {
@@ -163,31 +163,18 @@ Future<Map<String, MultipartFile>> _getFileMap(
163163

164164
Future<BaseRequest> _prepareRequest(
165165
Uri uri,
166-
Operation operation,
167-
HttpConfig config,
166+
Map<String, dynamic> body,
167+
Map<String, String> httpHeaders,
168168
) async {
169-
final httpHeaders = config.headers;
170-
final body = _buildBody(operation, config);
171-
172169
final Map<String, MultipartFile> fileMap = await _getFileMap(body);
173170
if (fileMap.isEmpty) {
174-
if (operation.isQuery && config.http.useGETForQueries) {
175-
config.options['method'] = 'GET';
176-
}
177-
178-
final httpMethod = config.options['method']?.toString()?.toUpperCase() ?? 'POST';
179-
if (httpMethod == 'GET') {
180-
uri = uri.replace(queryParameters: body.map((k, v) => MapEntry(k, v is String ? v : json.encode(v))));
181-
}
182-
final Request r = Request(httpMethod, uri);
171+
final Request r = Request('post', uri);
183172
r.headers.addAll(httpHeaders);
184-
if (httpMethod != 'GET') {
185-
r.body = json.encode(body);
186-
}
173+
r.body = json.encode(body);
187174
return r;
188175
}
189176

190-
final MultipartRequest r = MultipartRequest('POST', uri);
177+
final MultipartRequest r = MultipartRequest('post', uri);
191178
r.headers.addAll(httpHeaders);
192179
r.fields['operations'] = json.encode(body, toEncodable: (dynamic object) {
193180
if (object is MultipartFile) {
@@ -230,67 +217,97 @@ Future<BaseRequest> _prepareRequest(
230217
return r;
231218
}
232219

233-
HttpConfig _mergeHttpConfigs(
220+
HttpHeadersAndBody _selectHttpOptionsAndBody(
221+
Operation operation,
234222
HttpConfig fallbackConfig, [
235223
HttpConfig linkConfig,
236224
HttpConfig contextConfig,
237225
]) {
226+
final Map<String, dynamic> options = <String, dynamic>{
227+
'headers': <String, String>{},
228+
'credentials': <String, dynamic>{},
229+
};
230+
final HttpQueryOptions http = HttpQueryOptions();
231+
238232
// http options
239-
final HttpQueryOptions httpQueryOptions = HttpQueryOptions();
240233

241234
// initialize with fallback http options
242-
httpQueryOptions.addAll(fallbackConfig.http);
235+
http.addAll(fallbackConfig.http);
243236

244237
// inject the configured http options
245238
if (linkConfig.http != null) {
246-
httpQueryOptions.addAll(linkConfig.http);
239+
http.addAll(linkConfig.http);
247240
}
248241

249242
// override with context http options
250243
if (contextConfig.http != null) {
251-
httpQueryOptions.addAll(contextConfig.http);
244+
http.addAll(contextConfig.http);
252245
}
253246

254-
return HttpConfig(
255-
http: httpQueryOptions,
256-
options: {
257-
...fallbackConfig.options,
258-
...(linkConfig != null ? linkConfig.options ?? {} : {}),
259-
...(contextConfig != null ? contextConfig.options ?? {} : {}),
260-
},
261-
credentials: {
262-
...fallbackConfig.credentials,
263-
...(linkConfig != null ? linkConfig.credentials ?? {} : {}),
264-
...(contextConfig != null ? contextConfig.credentials ?? {} : {}),
265-
},
266-
headers: {
267-
...fallbackConfig.headers,
268-
...(linkConfig != null ? linkConfig.headers ?? {} : {}),
269-
...(contextConfig != null ? contextConfig.headers ?? {} : {}),
270-
},
271-
);
272-
}
247+
// options
248+
249+
// initialize with fallback options
250+
options.addAll(fallbackConfig.options);
251+
252+
// inject the configured options
253+
if (linkConfig.options != null) {
254+
options.addAll(linkConfig.options);
255+
}
256+
257+
// override with context options
258+
if (contextConfig.options != null) {
259+
options.addAll(contextConfig.options);
260+
}
261+
262+
// headers
263+
264+
// initialze with fallback headers
265+
options['headers'].addAll(fallbackConfig.headers);
266+
267+
// inject the configured headers
268+
if (linkConfig.headers != null) {
269+
options['headers'].addAll(linkConfig.headers);
270+
}
271+
272+
// inject the context headers
273+
if (contextConfig.headers != null) {
274+
options['headers'].addAll(contextConfig.headers);
275+
}
276+
277+
// credentials
278+
279+
// initialze with fallback credentials
280+
options['credentials'].addAll(fallbackConfig.credentials);
281+
282+
// inject the configured credentials
283+
if (linkConfig.credentials != null) {
284+
options['credentials'].addAll(linkConfig.credentials);
285+
}
286+
287+
// inject the context credentials
288+
if (contextConfig.credentials != null) {
289+
options['credentials'].addAll(contextConfig.credentials);
290+
}
273291

274-
Map<String, dynamic> _buildBody(
275-
Operation operation,
276-
HttpConfig config,
277-
) {
278292
// the body depends on the http options
279293
final Map<String, dynamic> body = <String, dynamic>{
280294
'operationName': operation.operationName,
281295
'variables': operation.variables,
282-
};
296+
};
283297

284298
// not sending the query (i.e persisted queries)
285-
if (config.http.includeExtensions) {
299+
if (http.includeExtensions) {
286300
body['extensions'] = operation.extensions;
287301
}
288302

289-
if (config.http.includeQuery) {
303+
if (http.includeQuery) {
290304
body['query'] = printNode(operation.documentNode);
291305
}
292306

293-
return body;
307+
return HttpHeadersAndBody(
308+
headers: options['headers'] as Map<String, String>,
309+
body: body,
310+
);
294311
}
295312

296313
Future<FetchResult> _parseResponse(StreamedResponse response) async {
@@ -301,11 +318,13 @@ Future<FetchResult> _parseResponse(StreamedResponse response) async {
301318
final Uint8List responseByte = await response.stream.toBytes();
302319
final String decodedBody = encoding.decode(responseByte);
303320

304-
final Map<String, dynamic> jsonResponse =
305-
json.decode(decodedBody) as Map<String, dynamic>;
306-
final FetchResult fetchResult = FetchResult(
307-
statusCode: statusCode,
308-
);
321+
Map<String, dynamic> jsonResponse;
322+
try {
323+
jsonResponse= json.decode(decodedBody) as Map<String, dynamic>;
324+
}catch(e){
325+
throw ClientException('Invalid response body: $decodedBody');
326+
}
327+
final FetchResult fetchResult = FetchResult();
309328

310329
if (jsonResponse['errors'] != null) {
311330
fetchResult.errors =

packages/graphql/test/link/http/link_http_test.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -610,12 +610,12 @@ void main() {
610610

611611
expect(
612612
exception,
613-
const TypeMatcher<UnhandledFailureWrapper>(),
613+
const TypeMatcher<ClientException>(),
614614
);
615615

616616
expect(
617-
(exception as UnhandledFailureWrapper).failure,
618-
const TypeMatcher<FormatException>(),
617+
(exception as ClientException).message,
618+
"Invalid response body: ",
619619
);
620620
});
621621

0 commit comments

Comments
 (0)