TanStack Query: need last-modified from response header
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!
Hello,
I'm running into the same exact issue to extract the filename of a downloaded file using responseType blob!
@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?
I have to check the modifications in my code required for this change, but this sounds like a good idea.
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
@topical @MeCapron would you want to modify the response from TanStack query so that
databecomesdata.dataand there'd be a new fielddata.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 @MeCapron would you want to modify the response from TanStack query so that
databecomesdata.dataand there'd be a new fielddata.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 can you share your code?
@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.
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?
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
),
});
};