openapi-ts / openapi-typescript

Generate TypeScript types from OpenAPI 3 specs
https://openapi-ts.dev
MIT License
5.86k stars 467 forks source link

204 response causes wrong `data` to be `undefined` #1933

Closed DjordyKoert closed 1 week ago

DjordyKoert commented 1 month ago

Description

Having a 204 response in combination with a 200 response in the generated type causes the returned data to become undefined

image

Generated openapi.ts file by openapi-typescript:

export interface operations {
    get_getOrdersByCustomerId: {
        parameters: {
            query?: {
                /** @description Limit the amount of orders returned */
                limit?: number;
                /** @description Offset results */
                offset?: number;
            };
            header?: never;
            path: {
                customerId: string;
            };
            cookie?: never;
        };
        requestBody?: never;
        responses: {
            /** @description Returns all orders for a customer */
            200: {
                headers: {
                    [name: string]: unknown;
                };
                content: {
                    "application/json": components["schemas"]["OrderDTO"][];
                };
            };
            /** @description No orders found for customer */
            204: {
                headers: {
                    [name: string]: unknown;
                };
                content?: never;
            };
        };
    };
}

Reproduction

Have an operation as defined above (with a 200 and a 204 with no content). Then attempt to use the openapi-fetch client for the operation.

Expected result

I expected the type a union of components["schemas"]["OrderDTO"][] | undefined and for me to not have to do a manual type assertion.

export type UseOpenApiFetchHookOptions<
    T extends keyof paths,
    TMethod extends keyof paths[T],
    TOperation extends paths[T][TMethod] = paths[T][TMethod],
> = ParamsOption<TOperation> & RequestBodyOption<TOperation> & {
    // add your custom options here
    reactQuery?: {
        enabled: boolean; // Note: React Query type’s inference is difficult to apply automatically, hence manual option passing here
        // add other React Query options as needed
    };
};

const getCustomerOrders: keyof paths = '/api/v1/customers/{customerId}/orders';
export const customerOrdersOptions = ({ params }: UseOpenApiFetchHookOptions<typeof getCustomerOrders, 'get'>) => queryOptions({
    placeholderData: keepPreviousData,
    queryFn: async ({ signal }) => {
        const { data, response } = await client.GET('/api/v1/customers/{customerId}/orders', {
            params,
            parseAs: 'json',
            signal,
        });

        if (!response.ok) {
            throw 'error';
        }

        if (response.status === 204) { // Unrelated, Fix for 204's returning an empty object {}
            return null;
        }

        return data as unknown as Array<components['schemas']['OrderDTO']>; // Assertion should not be needed here
    },
    queryKey: [
        getCustomerOrders,
        params,
    ],
});

Checklist

drwpow commented 1 month ago

Thanks for raising! This is definitely a bug. It’s caused by a | never in the union that throws a wrench into the type inference, but this should be avoidable. Possibly even with a NonNullable<T> wrapping the data

gbudiman commented 1 month ago

Came here to report similar issue. I noticed this started showing when upgrading openapi-fetch to 0.10.0 or higher (latest 0.12.x also affected).

Using openapi-typescript 7.4.1

samuelpucat commented 1 week ago

Hi, I have a similar problem

generated schema.d.ts:

export interface operations {
    ...
    deleteLoadBalancer: {
        parameters: { ... };
        requestBody?: never;
        responses: {
            /** @description OK */
            200: {
                headers: {
                    [name: string]: unknown;
                };
                content?: never;
            };
        };
    };
    ...
}

calling api:

const { data } = await client.DELETE('/load-balancers/{name}', {
    params: {
        header: { correlationId: generateCorrelationId() },
        path: { name: loadBalancerName },
        query: { projectId },
    },
    // parseAs: 'text',
});

throws error:

SyntaxError: Unexpected end of JSON input
    at u (index.js:164:44)

which is in this part of library:

// parse response (falling back to .text() when necessary)
if (response.ok) {
  // if "stream", skip parsing entirely
  if (parseAs === "stream") {
    return { data: response.body, response };
  }
  return { data: await response[parseAs](), response };      // line 164
}

Calling the API with parseAs: 'text', solves the error but I don't know whether it is the correct solution I feel like this should have been caught by this:

// handle empty content
if (response.status === 204 || response.headers.get("Content-Length") === "0") {
  return response.ok ? { data: undefined, response } : { error: undefined, response };
}

but this API doesn't return Content-Length header nor have 204 status