Skip to content

[plugin-axios] v5: throwOnError: true resolves non-2xx responses because validateStatus is explicitly undefined #734

Description

@mkarajohn

What version of kubb is running?

5.0.0-beta.97

What kind of platform do you use?

Linux

Your kubb.config.ts config file?

Minimal Kubb configuration


import { pluginAxios } from '@kubb/plugin-axios';
import { pluginTs } from '@kubb/plugin-ts';
import { defineConfig } from 'kubb';

export default defineConfig({
  input: './openapi.yaml',
  output: {
    path: './src/api',
    clean: true,
  },
  plugins: [
    pluginTs(),
    pluginAxios(),
  ],
});

Swagger/OpenAPI file?

Minimal OpenAPI document


openapi: 3.1.0
info:
  title: Reproduction
  version: 1.0.0

paths:
  /login:
    post:
      operationId: login
      responses:
        "200":
          description: Successful login
          content:
            application/json:
              schema:
                type: object
                required: [token]
                properties:
                  token:
                    type: string
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                type: object
                required: [detail]
                properties:
                  detail:
                    type: string

What version of external packages are you using(@tanstack-query, MSW, React, Vue, ...)

{
  "axios": "1.17.0",
  "node": "24.16.0",
  "pnpm": "10.31.0",
  "kubb": "5.0.0-beta.97",
  "@kubb/plugin-axios": "5.0.0-beta.98",
  "@kubb/plugin-ts": "5.0.0-beta.98"
}

What steps can reproduce the bug?

  1. Generate the client with the configuration above.
  2. Configure the generated client to call a server that returns 401 for POST /login.
  3. Call the generated operation with throwOnError: true:
import { createClient } from './src/api/.kubb/client';
import { login } from './src/api/clients/login';

const client = createClient({
  baseURL: 'http://localhost:3000',
});

try {
  const result = await login({
    client,
    throwOnError: true,
  });

  console.log('resolved', result);
} catch (error) {
  console.log('rejected', error);
}
  1. Observe that the promise resolves with status: 401 instead of rejecting with ResponseError.

The Axios client generated by @kubb/plugin-axios does not reject non-2xx responses when throwOnError: true and no custom validateStatus function is provided.

The generated .kubb/client.ts contains:

const throwOnError = requestConfig.throwOnError ?? config.throwOnError ?? true;

const validateStatus =
  requestConfig.validateStatus ??
  config.validateStatus ??
  (throwOnError ? undefined : () => true);

It then includes validateStatus in the Axios request configuration even when its value is explicitly undefined.

Axios treats an explicit validateStatus: undefined as accepting every response status under its default transitional behavior:

https://axios.rest/pages/advanced/request-config#validatestatus

Consequently, a 401, 404, or 500 response resolves instead of rejecting. Kubb's catch block is never reached, so no generated ResponseError is thrown.

This also contradicts the generated TypeScript contract. When throwOnError: true, RequestResult is narrowed to successful response statuses, but at runtime it can resolve with:

{
  status: 401,
  data: undefined,
  error: { /* error body */ }
}

For React Query consumers, this can cause a failed HTTP request to resolve its query or mutation instead of entering the error state and invoking onError.

This happens every time a non-2xx response is returned with:

  • throwOnError: true
  • No request-level validateStatus
  • No client-level validateStatus

Suggested fix

Use an explicit 2xx validator when throwOnError is enabled:

const validateStatus =
  requestConfig.validateStatus ??
  config.validateStatus ??
  (throwOnError
    ? (status: number) => status >= 200 && status < 300
    : () => true);

This preserves custom request/client validators while making the default behavior agree with throwOnError, ResponseError, and the generated RequestResult type.

How often does this bug happen?

Every time

What is the expected behavior?

With throwOnError: true, only successful 2xx statuses should be accepted. Non-2xx responses should reject and be converted into Kubb's generated ResponseError.

With throwOnError: false, all statuses should continue resolving as discriminated { status, data, error } results.

Additional information

Axios documents that its default validator rejects statuses outside 200–299, but an explicitly supplied validateStatus: undefined resolves all statuses under the default transitional configuration:

https://axios.rest/pages/advanced/request-config#validatestatus

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions