HTTP Transport

Hot Chocolate implements the GraphQL over HTTP specification. The specification is a draft whose status code rules change between revisions, and the revision the server follows is selected with HttpTransportVersion, see Transport Versions.

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 headerFormatUse case
application/graphql-response+jsonSingle JSON resultStandard queries and mutations (default)
multipart/mixedMultipartIncremental delivery (@defer/@stream), batching
text/event-streamServer-Sent EventsSubscriptions, streaming, incremental delivery
application/jsonlJSON LinesStreaming, 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, the response Content-Type is application/json. Under Draft20250508, the default transport version, every request the server reads is then answered with a 200 status code, including one that fails validation or asks for an operation kind the request method does not allow; only a request it cannot read, such as a body that is not valid JSON or a request that is not a well-formed GraphQL over HTTP request, and a batch it does not accept have a 400 status code. Under Draft20260903, the response takes the same status code as application/graphql-response+json, and only a 2xx response carries Content-Type: application/json.

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:

HTTP
POST /graphql
HOST: foo.example
Content-Type: application/json

{
  "query": "query($id: ID!){user(id:$id){name}}",
  "variables": { "id": "QVBJcy5ndXJ1" }
}

response:

HTTP
HTTP/1.1 200 OK
Content-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:

GraphQL
query ($id: ID!) {
  user(id: $id) {
    name
  }
}

With the following query variables:

JSON
{
  "id": "QVBJcy5ndXJ1"
}

This request could be sent via an HTTP GET as follows:

request:

HTTP
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
HTTP/1.1 200 OK
Content-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:

C#
public class CustomHttpResponseFormatter : DefaultHttpResponseFormatter
{
    // ...
}

Register the formatter:

C#
builder.Services.AddHttpResponseFormatter<CustomHttpResponseFormatter>();

If you want to pass HttpResponseFormatterOptions to a custom formatter, make the following adjustments:

C#
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.

C#
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:

C#
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:

C#
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:

C#
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 headerTransportContent-Type
multipart/mixedMultipartmultipart/mixed
text/event-streamSSEtext/event-stream
application/jsonlJSON Linesapplication/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) uses pending, incremental with id, and completed to track deferred fragments:

JSON
{"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:

JSON
{"data":{"product":{"name":"Abc"}},"hasNext":true}
{"incremental":[{"data":{"description":"Abc desc"},"path":["product"]}],"hasNext":false}

The default format is 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.1
Accept: text/event-stream; incrementalSpec=v0.2
Accept: 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:

C#
builder
    .AddGraphQL()
    .AddHttpResponseFormatter(
        incrementalDeliveryFormat: IncrementalDeliveryFormat.Version_0_1);

Or with the options overload:

C#
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.

Transport Versions#

The GraphQL over HTTP specification is a draft, and its status code rules have changed between revisions. HttpResponseFormatterOptions.HttpTransportVersion selects the revision the server follows:

C#
builder
    .AddGraphQL()
    .AddHttpResponseFormatter(
        new HttpResponseFormatterOptions
        {
            HttpTransportVersion = HttpTransportVersion.Draft20260903
        });
VersionDescription
LatestThe default. Resolves to Draft20250508.
LegacyPredates the specification. A missing Accept header or */* is answered as application/json, and every application/json response has a 200 status code.
Draft20230127Resolves to Draft20250508.
Draft20250508The specification revision of 2025-05-08.
Draft20260903The specification revision of 2026-09-03, see below.

A value outside this list throws an ArgumentOutOfRangeException when the formatter is registered.

Draft20260903#

Draft20260903 changes the following compared to Draft20250508:

  • An application/json response takes the status code of application/graphql-response+json, and only a 2xx response carries Content-Type: application/json. Under Draft20250508, an application/json response has a 200 status code for every well-formed request and a 400 status code for a request the server cannot interpret.
  • A result that carries both data and errors has a 294 status code. Under Draft20250508, it has a 200 status code.
  • A request the server read but cannot execute has a 422 status code: a request that is not a well-formed GraphQL over HTTP request, a document that fails validation, an operation that cannot be determined, and variables that cannot be coerced. Under Draft20250508, these requests have a 400 status code for application/graphql-response+json; for application/json, only the request that is not well-formed has a 400 status code and the others have 200. A request body that is not valid JSON has a 400 status code under both. A GraphQL document that cannot be parsed has a 400 status code under both for application/graphql-response+json, and a 200 status code under Draft20250508 for application/json.
  • A request on the GraphQL endpoint whose method the endpoint does not support has a 405 status code and an Allow header listing the supported methods, an OPTIONS request has a 204 status code with the same header, and a POST request whose Content-Type the endpoint does not support has a 415 status code. Under Draft20250508, all three have a 404 status code.
Note

294 is not registered with IANA. Clients and intermediaries that do not recognize it treat it as 200 per RFC 9110, and it is not heuristically cacheable, so a response without cache headers is not stored. Infrastructure that acts on a fixed list of status codes can still treat it differently from 200. nginx's add_header directive, for example, emits headers only for a fixed list of codes unless the always flag is set, so CORS and security headers added that way are missing on a 294 response. Before enabling Draft20260903, verify that headers and caching behave as intended for 294 through your own infrastructure.

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/json request 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:

C#
builder.Services.AddHttpResponseFormatter(new HttpResponseFormatterOptions {
    HttpTransportVersion = HttpTransportVersion.Legacy
});

An Accept header with the value application/json makes the response Content-Type application/json. Under Legacy and Draft20250508, it also opts the client out of the status codes of the 2025-05-08 revision of the GraphQL over HTTP specification: a status code of 200 is returned for every well-formed request, even if it had validation errors. Under Draft20260903, the specification's status codes apply to application/json as well, see Transport Versions.

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-protocolDescription
graphql-transport-wsThe modern protocol defined by the graphql-ws library. This is the recommended protocol for new projects.
graphql-wsThe legacy protocol defined by Apollo's subscriptions-transport-ws. Use this for backward compatibility with older clients.

The client lists the sub-protocols it supports in the standard WebSocket Sec-WebSocket-Protocol header during the handshake, ordered by preference. Hot Chocolate accepts the first listed sub-protocol it supports. If none of the listed sub-protocols is supported, the server closes the connection with close code 1002 (protocol error).

Enabling WebSocket Support#

You must register the ASP.NET Core WebSocket middleware before calling MapGraphQL(). Without this, WebSocket upgrade requests are not handled.

C#
var 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:

PropertyTypeDefaultDescription
ConnectionInitializationTimeoutTimeSpanTimeSpan.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.
KeepAliveIntervalTimeSpan?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:

C#
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:

C#
app.MapGraphQLWebSocket("/graphql/ws")
    .WithOptions(o =>
    {
        o.ConnectionInitializationTimeout = TimeSpan.FromSeconds(30);
        o.KeepAliveInterval = TimeSpan.FromSeconds(12);
    });

Connection Lifecycle#

A WebSocket connection follows this sequence:

  1. The client opens a WebSocket connection and lists the sub-protocols it supports.
  2. The client sends a connection_init message within the ConnectionInitializationTimeout window.
  3. The server responds with connection_ack.
  4. The client subscribes to operations by sending subscribe messages.
  5. The server pushes results via next messages.
  6. When an operation completes, the server sends a complete message.
  7. The server sends periodic keep-alive pings at the KeepAliveInterval.
  8. 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: @defer and @stream results can be streamed via SSE.
  • Browser compatibility: The browser EventSource API natively supports SSE without additional libraries.

SSE Wire Format#

The server sends each result as an SSE event:

event: next
data: {"data":{"onMessageReceived":{"body":"Hello"}}}

event: next
data: {"data":{"onMessageReceived":{"body":"World"}}}

event: complete
data:

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.

PropertyTypeDefaultDescription
EnforceGetRequestsPreflightHeaderboolfalseWhen true, HTTP GET requests must include a preflight header. Prevents a browser from issuing GET requests via <script> or <img> tags.
EnforceMultipartRequestsPreflightHeaderbooltrueWhen 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:

C#
builder
    .AddGraphQL()
    .ModifyServerOptions(o =>
    {
        o.EnforceGetRequestsPreflightHeader = true;
        o.EnforceMultipartRequestsPreflightHeader = true;
    });
C#
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.
Edit this page on GitHub
Maintained by ChilliCream. Last updated on by Glen