Fix: GraphQL 400 Bad Request Error (Query Syntax and Variable Errors)
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix GraphQL 400 Bad Request errors — malformed query syntax, variable type mismatches, missing required fields, schema validation failures, and how to debug GraphQL errors from Apollo and fetch.
The Error
A GraphQL request returns HTTP 400 with an error response:
{
"errors": [
{
"message": "Syntax Error: Expected Name, found <EOF>.",
"locations": [{ "line": 3, "column": 1 }]
}
]
}Or a variable type error:
{
"errors": [
{
"message": "Variable \"$id\" of type \"String\" used in position expecting type \"ID!\"."
}
]
}Or a field validation error:
{
"errors": [
{
"message": "Cannot query field \"username\" on type \"User\". Did you mean \"name\"?"
}
]
}Why This Happens
A GraphQL server runs every incoming request through three sequential phases: parse (turn the query string into an AST), validate (check the AST against the schema), and execute (resolve each field). A 400 response means the request never reached execute — it failed in parse or validate. That distinction matters because it tells you the body of the request, not the resolvers, contains the bug. The server has not touched your database; it has rejected the document as ill-formed.
The parse phase is unforgiving. A missing brace, an unescaped quote, or an inline variable declaration outside an operation definition produces Syntax Error: Expected ... with a line and column. Validate is where the schema does its work: it confirms each field exists on its parent type, each argument is the right scalar, and each variable in the query is supplied at the right type. The “Cannot query field” and “Variable used in position expecting” errors come from this phase. A subtle case is when you change a server-side schema (renaming username to name) but ship the new server before regenerating client-side queries — the client now sends queries that fail validation, and you get a 400 storm in production.
There is also a small set of transport-level reasons that look like GraphQL errors but are actually HTTP issues. The most common is sending the query as text/plain or as a form post; GraphQL servers strictly require application/json with { query, variables, operationName } (or, for GET, the same fields as URL parameters). Some servers also enforce CSRF protection — Apollo Server v4 requires the Apollo-Require-Preflight: true header or a non-simple Content-Type on multipart uploads. Miss either and you get a 400 with a CSRF message.
- Query syntax errors — missing braces, unclosed strings, wrong field syntax.
- Querying fields that don’t exist on the type — field name typo or using a field from a different type.
- Variable type mismatch — passing a
Stringwhere the schema expects anIDorInt. - Missing required variables — a query uses
$id: ID!but the variables object doesn’t includeid. - Sending the wrong Content-Type — GraphQL servers require
Content-Type: application/jsonwith a JSON body containing{ query, variables }. - Introspection disabled in production — queries that rely on
__schemaor__typefail with 400 in hardened production environments.
Version History That Changes the Failure Mode
The GraphQL ecosystem has had several inflection points in the last few years. The error messages and required headers depend on which server you are talking to:
- GraphQL spec — October 2021 release — Made the canonical reference for operations, variables, and validation. Earlier “RFC drafts” floating in the wild are not authoritative; rely on the 2021 spec or its 2023 working draft for current behavior.
- GraphQL multipart request spec — The community spec for file uploads. Originally enforced by
apollo-serverv2 transparently; v3 and later require explicit opt-in via thegraphql-uploadpackage because it is no longer bundled. - Apollo Server v2 (2018) — The original “all-in-one” server. Deprecated end-of-life October 2023. Permissive about Content-Type, bundled
graphql-upload, shipped with GraphQL Playground as the default landing page. - Apollo Server v3 (April 2021) — Removed file uploads from the core. Removed GraphQL Playground in favor of Apollo Sandbox at studio.apollographql.com. Introduced the “stand-alone” vs “framework integration” split.
- Apollo Server v4.0 (October 2022) — Standardized the integration API: one core package, separate integration packages for Express, Fastify, Lambda, etc. Mandated CSRF prevention by default — non-preflighted POSTs are rejected with a 400 unless
Apollo-Require-Preflight: trueis set or the Content-Type isapplication/json. This is the breaking change behind many “suddenly returns 400” reports in late 2022. - graphql-yoga v3 (December 2022) — Rewrite by The Guild on top of
@graphql-tools/executorand the WHATWG fetch API. Runs in Node, Bun, Deno, and edge runtimes. Default behavior accepts both GET and POST and uses landing page based on Yoga branding. - Apollo Sandbox (mid-2023) — Officially replaced GraphQL Playground. The legacy
playgroundmiddleware was removed; pointing your browser at/graphqlnow redirects to Sandbox in development or returns 400 in production hardened mode. - Persisted query manifest (2024) — Apollo Server v4.10 added first-class manifest support for safelisted persisted queries, with new
PersistedQueryNotFoundsemantics.
If you upgraded from Apollo Server v3 to v4 and saw a wave of 400 errors, the cause is almost always the CSRF default. Either send Content-Type: application/json on every request (Apollo treats that as preflighted) or add Apollo-Require-Preflight: true. If you migrated to graphql-yoga, the multipart upload spec is supported but you need the @graphql-yoga/plugin-graphql-multipart-request plugin enabled explicitly.
Fix 1: Fix Query Syntax Errors
GraphQL has strict syntax — a missing brace or wrong field structure causes immediate rejection:
Broken — missing closing brace:
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
# Missing closing brace for user
# Missing closing brace for queryBroken — wrong field selection syntax:
# Wrong — using = to assign
query {
users {
id = name # ← Syntax error
}
}
# Correct — just list fields
query {
users {
id
name
}
}Use an IDE with GraphQL language support to catch syntax errors before sending:
npm install --save-dev graphql-language-service-cli @graphql-eslint/eslint-pluginValidate queries against your schema locally:
npx graphql-inspector validate 'src/**/*.graphql' --schema schema.graphqlFix 2: Fix Variable Type Mismatches
GraphQL is strictly typed — variables must match the schema exactly:
Schema:
type Query {
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
}Broken — sending wrong variable types:
// Schema expects ID! but sending a number
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) { id name }
}
`;
// Wrong — variables don't match
client.query({
query: GET_USER,
variables: { id: 123 }, // Number — schema wants ID (string)
});Fixed:
client.query({
query: GET_USER,
variables: { id: '123' }, // String — matches ID type
});Common type mappings:
| GraphQL Type | JavaScript Type |
|---|---|
ID | string |
String | string |
Int | number (integer) |
Float | number |
Boolean | boolean |
Custom scalar DateTime | string (ISO 8601) or Date |
Check required vs optional variables:
# $id: ID! — required, must be provided
# $name: String — optional, can be omitted
query GetUser($id: ID!, $name: String) {
user(id: $id, name: $name) { id }
}// If $id is required and you omit it:
variables: { name: 'Alice' } // Error: Variable "$id" of required type "ID!" was not provided.
// Must always include required variables:
variables: { id: '1', name: 'Alice' }Fix 3: Fix the Request Format
GraphQL servers expect a specific JSON body format. Wrong content type or body structure causes 400:
Using fetch directly:
// Wrong — sending the query as plain text
const response = await fetch('/graphql', {
method: 'POST',
body: `query { users { id name } }`, // ← Must be JSON
});
// Correct — JSON body with query and variables
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json', // Required
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
query: `
query GetUsers($limit: Int) {
users(limit: $limit) {
id
name
email
}
}
`,
variables: { limit: 10 },
}),
});
const { data, errors } = await response.json();
if (errors) {
console.error('GraphQL errors:', errors);
}Using Apollo Client — ensure the link is configured correctly:
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const client = new ApolloClient({
link: new HttpLink({
uri: '/graphql',
headers: {
Authorization: `Bearer ${getToken()}`,
},
}),
cache: new InMemoryCache(),
});Using graphql-request:
import { GraphQLClient, gql } from 'graphql-request';
const client = new GraphQLClient('/graphql', {
headers: { Authorization: `Bearer ${token}` },
});
const GET_USERS = gql`
query GetUsers($limit: Int) {
users(limit: $limit) { id name }
}
`;
const data = await client.request(GET_USERS, { limit: 10 });Fix 4: Fix “Cannot query field” Errors
This means you are requesting a field that doesn’t exist on the type:
{
"message": "Cannot query field \"username\" on type \"User\". Did you mean \"name\"?"
}Check the actual schema:
# Introspection query — run this to get the User type fields
query {
__type(name: "User") {
fields {
name
type { name kind }
}
}
}Or use a GraphQL playground (Apollo Sandbox, GraphiQL) to browse the schema.
Fix the field name:
# Wrong — field doesn't exist
query {
user(id: "1") {
username # ← No such field
email_address # ← No such field
}
}
# Correct — use actual schema field names
query {
user(id: "1") {
name # ← Correct
email # ← Correct
}
}For nested type errors:
{
"message": "Cannot query field \"street\" on type \"User\". Did you mean to use an inline fragment on \"Address\"?"
}# Wrong — trying to access nested fields without drilling in
query {
user(id: "1") {
address.street # ← Not valid GraphQL syntax
}
}
# Correct — nest the selection set
query {
user(id: "1") {
address {
street
city
zip
}
}
}Fix 5: Handle GraphQL Errors in Your Application
GraphQL can return both HTTP 200 with errors (partial data) and HTTP 400 (request rejected). Handle both:
// Apollo Client — error handling
import { ApolloClient, from, HttpLink } from '@apollo/client';
import { onError } from '@apollo/client/link/error';
const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
console.error(`GraphQL Error: ${message}`, { locations, path });
if (extensions?.code === 'UNAUTHENTICATED') {
// Redirect to login
window.location.href = '/login';
}
});
}
if (networkError) {
console.error('Network error:', networkError);
// networkError.statusCode === 400 means bad request
if ('statusCode' in networkError && networkError.statusCode === 400) {
console.error('Bad GraphQL request — check query syntax and variables');
}
}
});
const client = new ApolloClient({
link: from([errorLink, new HttpLink({ uri: '/graphql' })]),
cache: new InMemoryCache(),
});In components — check for errors explicitly:
const { loading, error, data } = useQuery(GET_USER, {
variables: { id: userId },
});
if (error) {
// error.graphQLErrors — validation/resolver errors
// error.networkError — HTTP-level errors (400, 500, network failures)
const is400 = error.networkError?.statusCode === 400;
return <div>Error: {is400 ? 'Invalid query' : error.message}</div>;
}Fix 6: Fix Mutations with Input Types
Mutations often use input types — a common source of 400 errors:
Schema:
input CreateUserInput {
name: String!
email: String!
role: Role = USER
}
type Mutation {
createUser(input: CreateUserInput!): User!
}Broken — passing variables at the wrong level:
// Wrong — variables passed as top-level instead of nested in input
client.mutate({
mutation: CREATE_USER,
variables: { name: 'Alice', email: '[email protected]' },
});
// Also wrong query — missing 'input' wrapper
const CREATE_USER = gql`
mutation CreateUser($name: String!, $email: String!) {
createUser(name: $name, email: $email) { id } # ← Schema expects input: CreateUserInput!
}
`;Fixed:
const CREATE_USER = gql`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) { id name email }
}
`;
client.mutate({
mutation: CREATE_USER,
variables: {
input: { name: 'Alice', email: '[email protected]' }, // Nested in 'input'
},
});Fix 7: Enable GraphQL Persisted Queries Correctly
If your server uses Automatic Persisted Queries (APQ) and your client sends a hash that the server doesn’t recognise, it returns 400:
{
"errors": [{ "message": "PersistedQueryNotFound" }]
}Apollo Client handles this automatically — ensure the retry link is configured:
import { createPersistedQueryLink } from '@apollo/client/link/persisted-queries';
import { sha256 } from 'crypto-hash';
const persistedQueriesLink = createPersistedQueryLink({
sha256,
useGETForHashedQueries: true,
});
const client = new ApolloClient({
link: from([persistedQueriesLink, authLink, httpLink]),
cache: new InMemoryCache(),
});When the server returns PersistedQueryNotFound, Apollo Client automatically retries with the full query — no manual handling needed.
Still Not Working?
Use GraphiQL or Apollo Sandbox to test queries interactively. These tools provide schema-aware autocomplete and immediately show validation errors. Test your query there before putting it in code.
Log the exact request being sent:
// Log every GraphQL request
const loggingLink = new ApolloLink((operation, forward) => {
console.log('GraphQL Request:', {
operationName: operation.operationName,
query: operation.query.loc?.source.body,
variables: operation.variables,
});
return forward(operation);
});Check CSRF protection on the server. Some GraphQL servers require a CSRF token or specific headers. A missing CSRF token often returns 400 or 403:
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': getCsrfToken(), // May be required
'Apollo-Require-Preflight': 'true', // Required by some Apollo Server configs
},Check that your client schema matches the deployed server schema. When the backend is ahead of the frontend, queries reference fields that no longer exist. Run graphql-codegen against the production endpoint, then diff the regenerated types against your committed types — any deletions are queries that will now 400. Pin a schema version in CI and fail the build when it changes without a matching client update.
Check whether introspection is disabled in production. Apollo Server v4 disables introspection by default in production mode. If your dev tools or admin dashboard sends query { __schema { ... } } against production, you get a 400 with GraphQL introspection is not allowed. Either enable it with introspection: true for trusted callers, or proxy admin tooling through a separate authenticated endpoint that has introspection on.
Check that the request URL is correct. Apollo Sandbox sometimes shows the request going to /graphql even when the client is misconfigured to /api/graphql. Open DevTools Network and look at the actual URL on the failing request — a 400 from the wrong path can come from a non-GraphQL middleware that simply rejects unknown JSON bodies.
For related API errors, see Fix: CORS Access-Control-Allow-Origin Error, Fix: FastAPI 422 Unprocessable Entity, Fix: GraphQL N+1 Query, and Fix: GraphQL Error Handling Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: OpenAI API Not Working — RateLimitError, 401, 429, and Connection Issues
How to fix OpenAI API errors — RateLimitError (429), AuthenticationError (401), APIConnectionError, context length exceeded, model not found, and SDK v0-to-v1 migration mistakes.
Fix: GraphQL Error Handling Not Working — Errors Not Returned or Always 200 OK
How to fix GraphQL error handling — error extensions, partial data with errors, Apollo formatError, custom error classes, client-side error detection, and network vs GraphQL errors.
Fix: Stripe Webhook Signature Verification Failed
How to fix Stripe webhook signature verification errors — why Stripe-Signature header validation fails, how to correctly pass the raw request body, and how to debug webhook delivery in the Stripe dashboard.
Fix: Deno PermissionDenied — Missing --allow-read, --allow-net, and Other Flags
How to fix Deno PermissionDenied (NotCapable in Deno 2) errors — the right permission flags, path-scoped permissions, deno.json permission sets, and the Deno.permissions API.