-
-
Notifications
You must be signed in to change notification settings - Fork 722
/
DefaultHttpResponseFormatter.cs
476 lines (418 loc) · 18.1 KB
/
DefaultHttpResponseFormatter.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
using System.Net;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.Encodings.Web;
using System.Text.Json;
using HotChocolate.Execution.Serialization;
using Microsoft.AspNetCore.Http;
using static HotChocolate.AspNetCore.AcceptMediaTypeKind;
using static HotChocolate.Execution.ExecutionResultKind;
namespace HotChocolate.AspNetCore.Serialization;
/// <summary>
/// This represents the default implementation for the <see cref="IHttpResponseFormatter" />
/// that abides by the GraphQL over HTTP specification.
/// https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md
/// </summary>
public class DefaultHttpResponseFormatter : IHttpResponseFormatter
{
private readonly JsonResultFormatter _jsonFormatter;
private readonly MultiPartResultFormatter _multiPartFormatter;
private readonly EventStreamResultFormatter _eventStreamResultFormatter;
/// <summary>
/// Creates a new instance of <see cref="DefaultHttpResponseFormatter" />.
/// </summary>
/// <param name="indented">
/// Defines whether the underlying <see cref="Utf8JsonWriter"/>
/// should pretty print the JSON which includes:
/// indenting nested JSON tokens, adding new lines, and adding
/// white space between property names and values.
/// By default, the JSON is written without extra white spaces.
/// </param>
/// <param name="encoder">
/// Gets or sets the encoder to use when escaping strings, or null to use the default encoder.
/// </param>
public DefaultHttpResponseFormatter(
bool indented = false,
JavaScriptEncoder? encoder = null)
{
_jsonFormatter = new JsonResultFormatter(indented, encoder);
_multiPartFormatter = new MultiPartResultFormatter(_jsonFormatter);
_eventStreamResultFormatter = new EventStreamResultFormatter(indented, encoder);
}
public GraphQLRequestFlags CreateRequestFlags(
AcceptMediaType[] acceptMediaTypes)
{
if (acceptMediaTypes.Length == 0)
{
return GraphQLRequestFlags.AllowLegacy;
}
var flags = GraphQLRequestFlags.None;
ref var searchSpace = ref MemoryMarshal.GetReference(acceptMediaTypes.AsSpan());
for (var i = 0; i < acceptMediaTypes.Length; i++)
{
var acceptMediaType = Unsafe.Add(ref searchSpace, i);
flags |= CreateRequestFlags(acceptMediaType);
if (flags is GraphQLRequestFlags.AllowAll)
{
return GraphQLRequestFlags.AllowAll;
}
}
return flags;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected virtual GraphQLRequestFlags CreateRequestFlags(
AcceptMediaType acceptMediaType)
{
var flags = GraphQLRequestFlags.None;
if (acceptMediaType.Kind is ApplicationGraphQL or ApplicationJson or AllApplication)
{
flags |= GraphQLRequestFlags.AllowQuery;
flags |= GraphQLRequestFlags.AllowMutation;
}
if (acceptMediaType.Kind is MultiPartMixed or AllMultiPart)
{
flags |= GraphQLRequestFlags.AllowQuery;
flags |= GraphQLRequestFlags.AllowMutation;
flags |= GraphQLRequestFlags.AllowStreams;
}
if (acceptMediaType.Kind is EventStream or All)
{
flags = GraphQLRequestFlags.AllowAll;
}
return flags;
}
public async ValueTask FormatAsync(
HttpResponse response,
IExecutionResult result,
AcceptMediaType[] acceptMediaTypes,
HttpStatusCode? proposedStatusCode,
CancellationToken cancellationToken)
{
if (!TryGetFormatter(result, acceptMediaTypes, out var format))
{
// we should not hit this point except if a middleware did not validate the
// GraphQL request flags which would indicate that there is no way to execute
// the GraphQL request with the specified accept header content types.
throw ThrowHelper.Formatter_InvalidAcceptMediaType();
}
if (result.Kind is SingleResult)
{
var queryResult = (IQueryResult)result;
var statusCode = (int)GetStatusCode(queryResult, format, proposedStatusCode);
response.ContentType = format.ContentType;
response.StatusCode = statusCode;
await format.Formatter.FormatAsync(result, response.Body, cancellationToken);
}
else if (result.Kind is DeferredResult or BatchResult or SubscriptionResult)
{
var responseStream = (IResponseStream)result;
var statusCode = (int)GetStatusCode(responseStream, format, proposedStatusCode);
response.ContentType = format.ContentType;
response.StatusCode = statusCode;
await format.Formatter.FormatAsync(result, response.Body, cancellationToken);
}
else
{
// we should not hit this point except in the case that we introduce a new
// ExecutionResultKind and forget to update this method.
throw ThrowHelper.Formatter_ResultKindNotSupported();
}
}
protected virtual HttpStatusCode GetStatusCode(
IQueryResult result,
FormatInfo format,
HttpStatusCode? proposedStatusCode)
{
// the current spec proposal strongly recommend to always return OK
// when using the legacy application/json response content-type.
if (format.Kind is ResponseContentType.Json)
{
return HttpStatusCode.OK;
}
// if we are sending a single result with the multipart/mixed header or
// with a text/event-stream response content-type we as well will just
// respond with a OK status code.
if (format.Kind is ResponseContentType.MultiPartMixed or ResponseContentType.EventStream)
{
return HttpStatusCode.OK;
}
// in the case of the application/graphql-response+json we will
// use status code to indicate certain kinds of error categories.
if (format.Kind is ResponseContentType.GraphQLResponse)
{
// if a status code was proposed by the middleware we will in general accept it.
// the middleware are implement in a way that they will propose status code for
// the application/graphql-response+json response content-type.
if (proposedStatusCode.HasValue)
{
return proposedStatusCode.Value;
}
// if the GraphQL result has context data we will check if some middleware provided
// a status code or indicated an error that should be interpreted as an status code.
if (result.ContextData is not null)
{
var contextData = result.ContextData;
// first we check if there is an explicit HTTP status code override by the user.
if (contextData.TryGetValue(WellKnownContextData.HttpStatusCode, out var value) &&
value is HttpStatusCode statusCode)
{
return statusCode;
}
// next we check if the validation of the request failed.
// if that is the case we will we will return a BadRequest status code (400).
if (contextData.ContainsKey(WellKnownContextData.ValidationErrors))
{
return HttpStatusCode.BadRequest;
}
if (result.ContextData.ContainsKey(WellKnownContextData.OperationNotAllowed))
{
return HttpStatusCode.MethodNotAllowed;
}
}
// if data is not null then we have a valid result. The result of executing
// a GraphQL operation may contain partial data as well as encountered errors.
// Errors that happen during execution of the GraphQL operation typically
// become part of the result, as long as the server is still able to produce
// a well-formed response.
if (result.Data is not null)
{
return HttpStatusCode.OK;
}
// if data is null we consider the result not valid and return a 500 if the user did
// not override the status code with a different status code.
// this is however at the moment a point of discussion as there are opposing views
// towards what constitutes a valid response.
// we will update this status code as the spec moves towards release.
return HttpStatusCode.InternalServerError;
}
// we allow for users to implement alternative protocols or response content-type.
// if we end up here the user did not fully implement all necessary parts to add support
// for an alternative protocols or response content-type.
throw ThrowHelper.Formatter_ResponseContentTypeNotSupported(format.ContentType);
}
protected virtual HttpStatusCode GetStatusCode(
IResponseStream responseStream,
FormatInfo format,
HttpStatusCode? proposedStatusCode)
{
// if we are sending a response stream with the multipart/mixed header or
// with a text/event-stream response content-type we as well will just
// respond with a OK status code.
if (format.Kind is ResponseContentType.MultiPartMixed or ResponseContentType.EventStream)
{
return HttpStatusCode.OK;
}
// we allow for users to implement alternative protocols or response content-type.
// if we end up here the user did not fully implement all necessary parts to add support
// for an alternative protocols or response content-type.
throw ThrowHelper.Formatter_ResponseContentTypeNotSupported(format.ContentType);
}
private bool TryGetFormatter(
IExecutionResult result,
AcceptMediaType[] acceptMediaTypes,
out FormatInfo formatInfo)
{
formatInfo = default;
// if the request does not specify the accept header then we will
// use the `application/json` response content-type,
// which is the legacy behavior.
if (acceptMediaTypes.Length == 0)
{
if (result.Kind is SingleResult)
{
formatInfo = new FormatInfo(
ContentType.Json,
ResponseContentType.Json,
_jsonFormatter);
return true;
}
if (result.Kind is DeferredResult or BatchResult)
{
formatInfo = new FormatInfo(
ContentType.MultiPartMixed,
ResponseContentType.MultiPartMixed,
_multiPartFormatter);
return true;
}
if (result.Kind is SubscriptionResult)
{
formatInfo = new FormatInfo(
ContentType.EventStream,
ResponseContentType.EventStream,
_eventStreamResultFormatter);
return true;
}
return false;
}
// if the request specifies at least one accept media-type we will
// determine which is best to use.
// For this we first determine which characteristics our GraphQL result has.
var resultKind = result.Kind switch
{
SingleResult => ResultKind.Single,
SubscriptionResult => ResultKind.Subscription,
_ => ResultKind.Stream
};
// if we just have one accept header we will try to determine which formatter to take.
// we should only be unable to find a match if there was a previous validation skipped.
if (acceptMediaTypes.Length == 1)
{
var mediaType = acceptMediaTypes[0];
if (resultKind is ResultKind.Single &&
mediaType.Kind is ApplicationGraphQL or AllApplication)
{
formatInfo = new FormatInfo(
ContentType.GraphQLResponse,
ResponseContentType.GraphQLResponse,
_jsonFormatter);
return true;
}
if (resultKind is ResultKind.Single &&
mediaType.Kind is ApplicationJson or All)
{
formatInfo = new FormatInfo(
ContentType.Json,
ResponseContentType.Json,
_jsonFormatter);
return true;
}
if (resultKind is ResultKind.Stream or ResultKind.Single &&
mediaType.Kind is MultiPartMixed or AllMultiPart or All)
{
formatInfo = new FormatInfo(
ContentType.MultiPartMixed,
ResponseContentType.MultiPartMixed,
_multiPartFormatter);
return true;
}
if (mediaType.Kind is EventStream)
{
formatInfo = new FormatInfo(
ContentType.EventStream,
ResponseContentType.EventStream,
_eventStreamResultFormatter);
return true;
}
return false;
}
// if we have more than one specified accept media-type we will try to find the best for
// our GraphQL result.
ref var searchSpace = ref MemoryMarshal.GetReference(acceptMediaTypes.AsSpan());
var success = false;
for (var i = 0; i < acceptMediaTypes.Length; i++)
{
var mediaType = Unsafe.Add(ref searchSpace, i);
if (resultKind is ResultKind.Single &&
mediaType.Kind is ApplicationGraphQL or AllApplication)
{
formatInfo = new FormatInfo(
ContentType.GraphQLResponse,
ResponseContentType.GraphQLResponse,
_jsonFormatter);
return true;
}
if (resultKind is ResultKind.Single &&
mediaType.Kind is ApplicationJson or All)
{
// application/json is a legacy response content-type.
// We will create a formatInfo but keep on validating for
// a better suited format.
formatInfo = new FormatInfo(
ContentType.Json,
ResponseContentType.Json,
_jsonFormatter);
success = true;
}
if (resultKind is ResultKind.Stream or ResultKind.Single &&
mediaType.Kind is MultiPartMixed or AllMultiPart or All)
{
// if the result is a stream we consider this a perfect match and
// will use this format.
if (resultKind is ResultKind.Stream)
{
formatInfo = new FormatInfo(
ContentType.MultiPartMixed,
ResponseContentType.MultiPartMixed,
_multiPartFormatter);
return true;
}
// if the format is a event-stream or not set we will create a
// multipart/mixed formatInfo for the current result but also keep
// on validating for a better suited format.
if (formatInfo.Kind is not ResponseContentType.Json)
{
formatInfo = new FormatInfo(
ContentType.MultiPartMixed,
ResponseContentType.MultiPartMixed,
_multiPartFormatter);
success = true;
}
}
if (mediaType.Kind is EventStream or All)
{
// if the result is a subscription we consider this a perfect match and
// will use this format.
if (resultKind is ResultKind.Stream)
{
formatInfo = new FormatInfo(
ContentType.EventStream,
ResponseContentType.EventStream,
_eventStreamResultFormatter);
return true;
}
// if the result is stream it means that we did not yet validated a
// multipart content-type and thus will create a format for the case that it
// is not specified;
// or we have a single result but there is no format yet specified
// we will create a text/event-stream formatInfo for the current result
// but also keep on validating for a better suited format.
if (formatInfo.Kind is ResponseContentType.Unknown)
{
formatInfo = new FormatInfo(
ContentType.MultiPartMixed,
ResponseContentType.MultiPartMixed,
_multiPartFormatter);
success = true;
}
}
}
return success;
}
/// <summary>
/// Representation of a resolver format, containing the formatter and the content type.
/// </summary>
protected readonly struct FormatInfo
{
/// <summary>
/// Initializes a new instance of <see cref="FormatInfo"/>.
/// </summary>
public FormatInfo(
string contentType,
ResponseContentType kind,
IExecutionResultFormatter formatter)
{
ContentType = contentType;
Kind = kind;
Formatter = formatter;
}
/// <summary>
/// Gets the response content type.
/// </summary>
public string ContentType { get; }
/// <summary>
/// Gets an enum value representing well-known response content types.
/// This prop is an optimization that helps avoiding comparing strings.
/// </summary>
public ResponseContentType Kind { get; }
/// <summary>
/// Gets the formatter that creates the body of the HTTP response.
/// </summary>
public IExecutionResultFormatter Formatter { get; }
}
private enum ResultKind
{
Single,
Stream,
Subscription
}
}