Skip to content

Fix: GraphQL 400 Bad Request Error (Query Syntax and Variable Errors)

FixDevs · (Updated: )

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 String where the schema expects an ID or Int.
  • Missing required variables — a query uses $id: ID! but the variables object doesn’t include id.
  • Sending the wrong Content-Type — GraphQL servers require Content-Type: application/json with a JSON body containing { query, variables }.
  • Introspection disabled in production — queries that rely on __schema or __type fail 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-server v2 transparently; v3 and later require explicit opt-in via the graphql-upload package 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: true is set or the Content-Type is application/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/executor and 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 playground middleware was removed; pointing your browser at /graphql now 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 PersistedQueryNotFound semantics.

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 query

Broken — 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-plugin

Validate queries against your schema locally:

npx graphql-inspector validate 'src/**/*.graphql' --schema schema.graphql

Fix 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 TypeJavaScript Type
IDstring
Stringstring
Intnumber (integer)
Floatnumber
Booleanboolean
Custom scalar DateTimestring (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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles