HTTP Transport
Hot Chocolate implements the latest version of the GraphQL over HTTP specification.
Response Formats and Content Negotiation
Hot Chocolate uses the HTTP Accept header to determine how to format the response. Four response formats are available:
| Accept header | Format | Use case |
|---|---|---|
application/graphql-response+json | Single JSON result | Standard queries and mutations (default) |
multipart/mixed | Multipart | Incremental delivery (@defer/@stream), batching |
text/event-stream | Server-Sent Events | Subscriptions, streaming, incremental delivery |
application/jsonl | JSON Lines | Streaming, batch responses |
When a client sends no Accept header or sends */*, the server responds with application/graphql-response+json for single results. For streaming operations, the server defaults to multipart/mixed unless the client explicitly requests a different format.
When the client sends Accept: application/json, it opts out of the GraphQL over HTTP specification and receives legacy-style responses with a 200 status code for all requests.
Types of Requests
GraphQL requests over HTTP can be performed via either the POST or GET HTTP verb.
POST Requests
The GraphQL HTTP POST request is the most commonly used variant for GraphQL requests over HTTP and is specified here.
request:
POST /graphqlHOST: foo.exampleContent-Type: application/json
{ "query": "query($id: ID!){user(id:$id){name}}", "variables": { "id": "QVBJcy5ndXJ1" }}
response:
HTTP/1.1 200 OKContent-Type: application/json
{ "data": { "user": { "name": "Jon Doe" } }}
GET Requests
GraphQL can also be served through an HTTP GET request. You have the same options as the HTTP POST request, but the request properties are provided as query parameters. GraphQL HTTP GET requests can be a good choice when you want to cache GraphQL requests.
For example, if you wanted to execute the following GraphQL query:
query ($id: ID!) { user(id: $id) { name }}
With the following query variables:
{ "id": "QVBJcy5ndXJ1"}
This request could be sent via an HTTP GET as follows:
request:
GET /graphql?query=query(%24id%3A%20ID!)%7Buser(id%3A%24id)%7Bname%7D%7D&variables=%7B%22id%22%3A%22QVBJcy5ndXJ1%22%7D`HOST: foo.example
response:
HTTP/1.1 200 OKContent-Type: application/json
{ "data": { "user": { "name": "Jon Doe" } }}
Note: {query} and {operationName} parameters are encoded as raw strings in the query component. Therefore if the query string contained operationName=null then it should be interpreted as the {operationName} being the string "null". If a literal null is desired, the parameter (e.g. {operationName}) should be omitted.
The GraphQL HTTP GET request is specified here.
DefaultHttpResponseFormatter
The DefaultHttpResponseFormatter abstracts how responses are delivered over HTTP.
You can override certain aspects of the formatter by creating your own formatter that inherits from DefaultHttpResponseFormatter:
public class CustomHttpResponseFormatter : DefaultHttpResponseFormatter{ // ...}
Register the formatter:
builder.Services.AddHttpResponseFormatter<CustomHttpResponseFormatter>();
If you want to pass HttpResponseFormatterOptions to a custom formatter, make the following adjustments:
var options = new HttpResponseFormatterOptions();
builder.Services.AddHttpResponseFormatter(_ => new CustomHttpResponseFormatter(options));
public class CustomHttpResponseFormatter : DefaultHttpResponseFormatter{ public CustomHttpResponseFormatter(HttpResponseFormatterOptions options) : base(options) {
}}
Customizing Status Codes
You can use a custom formatter to alter the HTTP status code in certain conditions.
Warning: Altering status codes can break the assumptions of your server's clients and might lead to issues. Proceed with caution.
public class CustomHttpResponseFormatter : DefaultHttpResponseFormatter{ protected override HttpStatusCode OnDetermineStatusCode( IOperationResult result, FormatInfo format, HttpStatusCode? proposedStatusCode) { if (result.Errors?.Count > 0 && result.Errors.Any(error => error.Code == "SOME_AUTH_ISSUE")) { return HttpStatusCode.Forbidden; }
// In all other cases let Hot Chocolate figure out the // appropriate status code. return base.OnDetermineStatusCode(result, format, proposedStatusCode); }}
JSON Serialization
You can alter some JSON serialization settings when configuring the HttpResponseFormatter.
Stripping Nulls from Response
By default, the JSON in your GraphQL responses contains null. If you want to reduce payload size and your clients can handle it, strip nulls from responses:
var options = new HttpResponseFormatterOptions{ Json = new JsonResultFormatterOptions { NullIgnoreCondition = JsonNullIgnoreCondition.All }};
builder.Services.AddHttpResponseFormatter(options);
Indenting JSON in Response
By default, the JSON in your GraphQL responses is not indented. If you want to indent your JSON:
builder.Services.AddHttpResponseFormatter(indented: true);
Be aware that indenting JSON results in a slightly larger response size.
If you are defining other HttpResponseFormatterOptions, configure the indentation through the Json property:
var options = new HttpResponseFormatterOptions{ Json = new JsonResultFormatterOptions { Indented = true }};
builder.Services.AddHttpResponseFormatter(options);
Incremental Delivery (@defer / @stream)
When using @defer or @stream, Hot Chocolate streams results to the client using one of three transport formats, selected via the Accept header:
| Accept header | Transport | Content-Type |
|---|---|---|
multipart/mixed | Multipart | multipart/mixed |
text/event-stream | SSE | text/event-stream |
application/jsonl | JSON Lines | application/jsonl |
If no streaming Accept header is provided, the default is multipart/mixed.
Incremental Delivery Wire Format
There are two wire formats for how incremental results are represented in the response payload.
v0.2 (default in v16) uses pending, incremental with id, and completed to track deferred fragments:
{"data":{"product":{"name":"Abc"}},"pending":[{"id":"2","path":["product"]}],"hasNext":true}{"incremental":[{"id":"2","data":{"description":"Abc desc"}}],"completed":[{"id":"2"}],"hasNext":false}
v0.1 (legacy) uses path and label directly on incremental entries:
{"data":{"product":{"name":"Abc"}},"hasNext":true}{"incremental":[{"data":{"description":"Abc desc"},"path":["product"]}],"hasNext":false}
In v16, the default changed from v0.1 to v0.2. If your clients depend on the legacy format, you have two options: client-driven format selection or changing the server default.
Client-Driven Format Selection
Clients choose which format they want by adding the incrementalSpec parameter to the Accept header:
Accept: multipart/mixed; incrementalSpec=v0.1Accept: text/event-stream; incrementalSpec=v0.2Accept: application/jsonl; incrementalSpec=v0.1
When the client does not specify incrementalSpec, the server default is used.
Changing the Server Default
The default incremental delivery format is v0.2. To change it server-wide:
builder .AddGraphQL() .AddHttpResponseFormatter( incrementalDeliveryFormat: IncrementalDeliveryFormat.Version_0_1);
Or with the options overload:
builder .AddGraphQL() .AddHttpResponseFormatter( new HttpResponseFormatterOptions { /* ... */ }, incrementalDeliveryFormat: IncrementalDeliveryFormat.Version_0_1);
The server default is only used as a fallback. A client that sends incrementalSpec=v0.1 or incrementalSpec=v0.2 in the Accept header always gets the format it asked for, regardless of the server default.
Streaming Transports
Hot Chocolate supports three streaming transport formats for delivering result streams (incremental delivery, batching, and subscriptions). The client selects the format via the Accept header.
Multipart (multipart/mixed)
The default streaming transport. Each result is sent as a separate MIME part separated by a boundary string. This is the most widely supported format.
Accept: multipart/mixed
Server-Sent Events (text/event-stream)
Results are delivered as SSE events. This transport works well with browser EventSource APIs and proxies that support SSE.
Accept: text/event-stream
Each result is sent as an event: next message with the JSON payload in the data: field. A final event: complete message signals the end of the stream.
JSON Lines (application/jsonl)
Each result is written as a single line of JSON, separated by newlines. This format is compact and straightforward to parse incrementally, making it well-suited for batch responses.
Accept: application/jsonl
{"data":{"hero":{"name":"R2-D2"}}}{"data":{"hero":{"name":"Luke Skywalker"}}}
The server sends periodic keep-alive messages (a space followed by a newline) to prevent connection timeouts.
Batching
Hot Chocolate supports operation batching, request batching, and variable batching. These features let you send and execute multiple GraphQL operations in a single HTTP request, with results streamed back using one of the transport formats above.
For full details on how to enable and use batching, see the Batching page.
Supporting Legacy Clients
Your clients might not yet support the GraphQL over HTTP specification. This can be problematic if they cannot handle a different response Content-Type or HTTP status codes besides 200.
If you have control over the client, you can either:
- Update the client to support the GraphQL over HTTP specification
- Send the
Accept: application/jsonrequest header in your HTTP requests, signaling that your client only understands the legacy format
If you cannot update or change the Accept header your clients are sending, configure that a missing Accept header or a wildcard like */* should be treated as application/json:
builder.Services.AddHttpResponseFormatter(new HttpResponseFormatterOptions { HttpTransportVersion = HttpTransportVersion.Legacy});
An Accept header with the value application/json opts you out of the GraphQL over HTTP specification. The response Content-Type becomes application/json and a status code of 200 is returned for every request, even if it had validation errors or a valid response could not be produced.
WebSocket Transport
Hot Chocolate supports GraphQL over WebSocket for real-time communication, including subscriptions. WebSocket connections stay open, allowing the server to push results to the client as they become available.
Supported Sub-Protocols
Hot Chocolate supports two WebSocket sub-protocols:
| Sub-protocol | Description |
|---|---|
graphql-transport-ws | The modern protocol defined by the graphql-ws library. This is the recommended protocol for new projects. |
graphql-ws | The legacy protocol defined by Apollo's subscriptions-transport-ws. Use this for backward compatibility with older clients. |
The client selects its preferred sub-protocol via the standard WebSocket Sec-WebSocket-Protocol header during the handshake. Hot Chocolate negotiates and accepts whichever protocol the client requests.
Enabling WebSocket Support
You must register the ASP.NET Core WebSocket middleware before calling MapGraphQL(). Without this, WebSocket upgrade requests are not handled.
// Program.csvar builder = WebApplication.CreateBuilder(args);
builder .AddGraphQL() .AddQueryType<Query>() .AddSubscriptionType<Subscription>();
var app = builder.Build();
app.UseWebSockets(); // Required before MapGraphQL()app.MapGraphQL();
app.Run();
WebSocket Options
The GraphQLSocketOptions class controls WebSocket behavior:
| Property | Type | Default | Description |
|---|---|---|---|
ConnectionInitializationTimeout | TimeSpan | TimeSpan.FromSeconds(10) | The time a client has to send a connection_init message after opening the WebSocket. If the client does not initialize within this window, the server closes the connection. |
KeepAliveInterval | TimeSpan? | TimeSpan.FromSeconds(5) | The interval at which the server sends keep-alive pings to prevent idle connections from being dropped. Set to null to disable keep-alive. |
Configure these options through ModifyServerOptions:
builder .AddGraphQL() .ModifyServerOptions(o => { o.Sockets.ConnectionInitializationTimeout = TimeSpan.FromSeconds(30); o.Sockets.KeepAliveInterval = TimeSpan.FromSeconds(12); });
You can also configure WebSocket options per-endpoint when using MapGraphQLWebSocket:
app.MapGraphQLWebSocket("/graphql/ws") .WithOptions(o => { o.ConnectionInitializationTimeout = TimeSpan.FromSeconds(30); o.KeepAliveInterval = TimeSpan.FromSeconds(12); });
Connection Lifecycle
A WebSocket connection follows this sequence:
- The client opens a WebSocket connection and specifies the sub-protocol.
- The client sends a
connection_initmessage within theConnectionInitializationTimeoutwindow. - The server responds with
connection_ack. - The client subscribes to operations by sending
subscribemessages. - The server pushes results via
nextmessages. - When an operation completes, the server sends a
completemessage. - The server sends periodic keep-alive pings at the
KeepAliveInterval. - Either side can close the connection.
Server-Sent Events (SSE)
Server-Sent Events provide an HTTP-based alternative to WebSocket for receiving streaming results. SSE is content-negotiated: the client requests it by sending Accept: text/event-stream on the standard GraphQL HTTP endpoint. There is no separate SSE endpoint.
SSE follows the GraphQL over SSE specification.
When to Use SSE
SSE is useful in the following scenarios:
- Subscriptions over HTTP: When WebSocket connections are blocked by firewalls, proxies, or load balancers, SSE provides an alternative path for receiving real-time updates.
- Incremental delivery:
@deferand@streamresults can be streamed via SSE. - Browser compatibility: The browser
EventSourceAPI natively supports SSE without additional libraries.
SSE Wire Format
The server sends each result as an SSE event:
event: nextdata: {"data":{"onMessageReceived":{"body":"Hello"}}}
event: nextdata: {"data":{"onMessageReceived":{"body":"World"}}}
event: completedata:
Each result is delivered as an event: next message with the JSON payload in the data: field. A final event: complete message signals the end of the stream.
SSE for Single Results
SSE is not limited to streaming. A client can send Accept: text/event-stream for a standard query, and the server responds with a single next event followed by complete. This can be useful when you want a uniform transport across all operation types.
Preflight Header Enforcement
Hot Chocolate provides two settings for enforcing preflight headers as a defense against cross-site request forgery (CSRF) attacks. These settings require that certain requests include a non-standard header (such as X-Requested-With or GraphQL-Preflight), which triggers a CORS preflight check in browsers.
| Property | Type | Default | Description |
|---|---|---|---|
EnforceGetRequestsPreflightHeader | bool | false | When true, HTTP GET requests must include a preflight header. Prevents a browser from issuing GET requests via <script> or <img> tags. |
EnforceMultipartRequestsPreflightHeader | bool | true | When true, multipart form requests must include a preflight header. Prevents a browser from submitting multipart forms via standard <form> elements. |
Configure these settings through ModifyServerOptions or per-endpoint via WithOptions:
builder .AddGraphQL() .ModifyServerOptions(o => { o.EnforceGetRequestsPreflightHeader = true; o.EnforceMultipartRequestsPreflightHeader = true; });
app.MapGraphQL().WithOptions(o =>{ o.EnforceGetRequestsPreflightHeader = true;});
If a request is rejected because it lacks the required preflight header, the server responds with a 400 Bad Request status.
Next Steps
- Endpoints for configuring the GraphQL middleware and per-endpoint options.
- Batching for details on variable batching and request batching.
- Subscriptions for defining subscription types and event publishing.
- Interceptors for hooking into WebSocket and HTTP request processing.
- Migrate from v15 to v16 for the incremental delivery migration details.