Fix: GraphQL 400 Bad Request Error (Query Syntax and Variable 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
GraphQL servers validate every request against the schema before execution. A 400 response means the request itself is malformed or invalid — the server refused to run the query at all. Common causes:
- 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.
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
},For related API errors, see Fix: CORS Access-Control-Allow-Origin Error and Fix: FastAPI 422 Unprocessable Entity.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
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: Express req.body Is undefined
How to fix req.body being undefined in Express — missing body-parser middleware, wrong Content-Type header, middleware order issues, and multipart form data handling.
Fix: TypeScript Decorators Not Working (experimentalDecorators)
How to fix TypeScript decorators not applying — experimentalDecorators not enabled, emitDecoratorMetadata missing, reflect-metadata not imported, and decorator ordering issues.
Fix: CSS Custom Properties (Variables) Not Working or Not Updating
How to fix CSS custom properties not applying — wrong scope, missing fallback values, JavaScript not setting variables on the right element, and how CSS variables interact with media queries and Shadow DOM.