openapi-ts icon indicating copy to clipboard operation
openapi-ts copied to clipboard

TanStack Query: need last-modified from response header

Open topical opened this issue 7 months ago • 10 comments

Description

Hi,

I use the TanStack Query code generator and need access to response header.

In my scenario, REST-API requires 'If-Unmodified-Since' header to be passed to properly handle concurrent updates.

For this, I need to extract 'last-modified' from response headers. Actually, this looks like being almost impossible with code generated by HeyAPI.

E.g. I have an endpoint to retrieve information about a user. HeyAPI generates the following code for this:

export const getUserQueryQueryKey = (options?: OptionsLegacyParser<GetUserQueryData>) => [
  createQueryKey('getUserQuery', options),
]

export const getUserQueryOptions = (options?: OptionsLegacyParser<GetUserQueryData>) => {
  return queryOptions({
    queryFn: async ({ queryKey, signal }) => {
      // watch out: response header is discarded here!
      const { data } = await getUserQuery({
        ...options,
        ...queryKey[0],
        signal,
        throwOnError: true,
      })
      return data
    },
    queryKey: getUserQueryQueryKey(options),
  })
}

You see that the response header is discarded deeply inside.

Effectively, I have to rewrite getUserQueryQueryKey() and getUserQueryOptions(). That's kind of OK for a single API-endpoint, but not suitable for my application.

So: is there any idea to handle this more generic?

Thank you!

topical avatar May 23 '25 13:05 topical

Hello,

I'm running into the same exact issue to extract the filename of a downloaded file using responseType blob!

MeCapron avatar May 31 '25 23:05 MeCapron

@topical @MeCapron would you want to modify the response from TanStack query so that data becomes data.data and there'd be a new field data.headers?

mrlubos avatar May 31 '25 23:05 mrlubos

I have to check the modifications in my code required for this change, but this sounds like a good idea.

topical avatar Jun 04 '25 07:06 topical

Yeah I can't think of a better way to do this without introducing even more complexity. Sadly it will be a breaking change if you're already using the current version... in the future we could add codemods to make such migrations seamless, those don't exist today

mrlubos avatar Jun 04 '25 08:06 mrlubos

@topical @MeCapron would you want to modify the response from TanStack query so that data becomes data.data and there'd be a new field data.headers?

in my case I could bypass it by passing the file name back from my list since I have it. I checked the code once more and I can't either see a way where we could do that properly without breaking everything.

MeCapron avatar Jun 04 '25 09:06 MeCapron

@topical @MeCapron would you want to modify the response from TanStack query so that data becomes data.data and there'd be a new field data.headers?

in my case I could bypass it by passing the file name back from my list since I have it. I checked the code once more and I can't either see a way where we could do that properly without breaking everything.

👍

topical avatar Jun 04 '25 09:06 topical

@MeCapron can you share your code?

mrlubos avatar Jun 04 '25 10:06 mrlubos

@MeCapron can you share your code?

Nothing special linked to hey-api. In my case I just have a grid where I can download files. So basically, I already have the name of the file.

When clicking the "Download" button instead of getting the file name from the response headers of the blob, I just get it from the list.

It might be enough from most of the cases because the information I need is not only tied to the response headers.

MeCapron avatar Jun 04 '25 13:06 MeCapron

Are there any updates on this? I also want to have the response be returned instead of the parsed data, so that I can access response.headers as well. This applies for both queryFn and mutationFn. Maybe a response: "data" | "response" field for the plugin in the defineConfig?

abuyukyi101198 avatar Nov 05 '25 11:11 abuyukyi101198

I found a workaround that does not require changes to the package. Instead of using the react-query.gen.ts option functions, I created the utilities below that can be passed the SDK function and query key directly. Should be generic enough to use anywhere.

import {
  queryOptions,
  type QueryKey,
  type UseQueryOptions,
  type UseMutationOptions,
} from "@tanstack/react-query";

import type { Options } from "@/app/apiclient";
import type { RequestResult, TDataShape } from "@/app/apiclient/client";

type WithHeaders<T> = T & { headers?: Headers };

/**
 * Creates query options that include response headers in the returned data.
 * @param options - Optional query configuration
 * @param queryFn - API client SDK function that performs the query
 * @param queryKey - React Query key for caching and invalidation
 * @returns Query options with data augmented to include headers
 */
export const queryOptionsWithHeaders = <
  TResponses,
  TErrors,
  TData extends TDataShape = TDataShape,
  TQueryKey extends QueryKey = QueryKey,
>(
  options: Options<TData> | undefined,
  queryFn: <ThrowOnError extends boolean = true>(
    options: Options<TData, ThrowOnError>
  ) => RequestResult<TResponses, TErrors, ThrowOnError>,
  queryKey: TQueryKey
): UseQueryOptions<
  WithHeaders<TResponses[keyof TResponses]>,
  TErrors[keyof TErrors],
  WithHeaders<TResponses[keyof TResponses]>,
  TQueryKey
> => {
  return queryOptions<
    WithHeaders<TResponses[keyof TResponses]>,
    TErrors[keyof TErrors],
    WithHeaders<TResponses[keyof TResponses]>,
    TQueryKey
  >({
    queryFn: async ({ queryKey, signal }) => {
      const {
        data,
        response: { headers },
      } = await queryFn({
        ...options,
        ...(queryKey[0] as object),
        signal,
        throwOnError: true,
      } as Options<TData>);
      return { ...data, headers } as WithHeaders<TResponses[keyof TResponses]>;
    },
    queryKey,
  });
};

/**
 * Creates mutation options that include response headers in the returned data.
 * @param mutationOptions - Optional mutation configuration
 * @param mutationFn - API client SDK function that performs the mutation
 * @returns Mutation options with data augmented to include headers
 */
export const mutationOptionsWithHeaders = <
  TResponses,
  TErrors,
  TData extends TDataShape = TDataShape,
>(
  mutationOptions: Options<TData> | undefined,
  mutationFn: <ThrowOnError extends boolean = true>(
    options: Options<TData, ThrowOnError>
  ) => RequestResult<TResponses, TErrors, ThrowOnError>
): UseMutationOptions<
  WithHeaders<TResponses[keyof TResponses]>,
  TErrors[keyof TErrors],
  Options<TData>
> => {
  return {
    mutationFn: async (fnOptions) => {
      const {
        data,
        response: { headers },
      } = await mutationFn({
        ...mutationOptions,
        ...fnOptions,
        throwOnError: true,
      });
      return { ...data, headers } as WithHeaders<TResponses[keyof TResponses]>;
    },
  };
};

Here is how you would use it:

// Queries
export const useGetCurrentUser = (
  options?: Options<GetMeWithSummaryData> // <- options override
) => {
  return useQuery({
    ...queryOptionsWithHeaders<
      GetMeWithSummaryResponses, // <- XResponses type from types.gen.ts
      GetMeWithSummaryErrors, // <- XErrors type from types.gen.ts
      GetMeWithSummaryData // <- XData type from types.gen.ts
    >(
      options, 
      getMeWithSummary, // <- SDK function from sdk.gen.ts
      getMeWithSummaryQueryKey(options) // <- query key from react-query.gen.ts
    ),
  });
};

// Mutations
export const useAuthenticate = (
  mutationOptions?: Options<LoginData> // <- options override
) => {
  return useMutation({
    ...mutationOptionsWithHeaders<
      LoginResponses, // <- XResponses type from types.gen.ts
      LoginErrors, // <- XErrors type from types.gen.ts
      LoginData // <- XData type from types.gen.ts
    >(
      mutationOptions,
      login // <- SDK function from sdk.gen.ts
    ),
  });
};

abuyukyi101198 avatar Nov 18 '25 08:11 abuyukyi101198