orval icon indicating copy to clipboard operation
orval copied to clipboard

Axios: cannot customize the headers for requests

Open FLYSASA opened this issue 11 months ago • 3 comments

I have generated my code, like this image

but it seems I cannot configure headers for a specific API endpoint individually Content-Encoding': 'gzip. Is there any solution ?

My current temporary solution is:

export const useRecommendComponents = (queryOptions?: any) => {
  const mutation = usePostApiProjectsIdSimilarity({
    mutation: {
      ...queryOptions,
      mutationFn: async ({ id, data }) => {
        const paramsStr = JSON.stringify(data);
        const compressed = pako.gzip(encodeURIComponent(paramsStr));
        return axiosInstance({
          url: `/api/Projects/${id}/similarity`,
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Content-Encoding': 'gzip',
          },
          data: compressed,
        });
      },
    },
  });
  return mutation;
};

FLYSASA avatar Dec 18 '24 06:12 FLYSASA

i use Axios Request Interceptors to add custom headers. You could check the path and add the right content encoding.

/**
 * Axios utlity class for adding token handling and Date handling to all request/responses.
 */
export default class AxiosInterceptors {
	// hold instances of interceptor by their unique URL
	static requestInterceptors = new Map<string, number>();
	static responseInterceptors = new Map<string, number>();

	/**
	 * Configures Axios request/reponse interceptors to add JWT token.
	 *
	 * @param {AxiosInstance} instance the Axios instance to remove interceptors
	 * @param {string} token the JWT token to add to all Axios requests
	 */
	static setupAxiosInstance = (instance: AxiosInstance, token: string) => {
		const appKey = instance.defaults.baseURL!;
		EnvUtils.debug(`Configuring Axios request/response interceptors for: ${appKey}`);
		// Axios request interceptor to add JWT token
		const tokenRequestInterceptor = instance.interceptors.request.use(
			(config) => {
				if (token) {
					const headers = config.headers || {};
					headers.Authorization = `Bearer ${token}`;
				}
				return config;
			},
			(error) => {
				return Promise.reject(error);
			}
		);
		EnvUtils.debug(`Axios Token Request Interceptor: ${tokenRequestInterceptor}`);
		AxiosInterceptors.requestInterceptors.set(appKey, tokenRequestInterceptor);

		// Axios response interceptor to translate String to Date
		const dateResponseInterceptor = instance.interceptors.response.use((originalResponse) => {
			const auditHeader = originalResponse.headers['audit-event-id'];
			if (auditHeader) {
				window.sessionStorage.setItem('audit-event-id', auditHeader);
				window.localStorage.setItem('audit-event-id', auditHeader);
			}
			FormatUtils.translateJsonDates(originalResponse.data);
			return originalResponse;
		});
		EnvUtils.debug(`Axios Date Response Interceptor: ${dateResponseInterceptor}`);
		AxiosInterceptors.responseInterceptors.set(appKey, dateResponseInterceptor);
	};

	/**
	 * Cleanup Axios on sign out of application.
	 *
	 * @param {AxiosInstance} instance the Axios instance to remove interceptors
	 */
	static teardownAxiosInstance = (instance: AxiosInstance) => {
		const appKey = instance.defaults.baseURL!;
		EnvUtils.warn(`Cleaning up Axios removing all interceptors for: ${appKey}`);
		instance.interceptors.request.clear();
		instance.interceptors.response.clear();
	};
}

melloware avatar Dec 18 '24 12:12 melloware

Thank you very much for your answer. It's a good idea to add specific headers by checking the path in the axiosInstance interceptor. However, it would be even better if we could extend the orval-generated react-query request hooks to support passing axios configurations,

for usage example:

import {
  postApiProjectsIdSimilarity,
  usePostApiProjectsIdSimilarity,
} from '@/api/projects/projects';

export const useRecommendComponents = (queryOptions?: any) => {
  const mutation = usePostApiProjectsIdSimilarity({
    mutation: {
      ...queryOptions,
      mutationFn: async ({ id, data }) => {
        const paramsStr = JSON.stringify(data);
        const compressed = pako.gzip(encodeURIComponent(paramsStr));
        return postApiProjectsIdSimilarity(id, compressed);
      },
    },
    axiosConfig: {
      headers: {
        'Content-Type': 'application/json',
        'Content-Encoding': 'gzip',
      },
    },
  });
  return mutation;
};

for orval generate api:

image

FLYSASA avatar Dec 19 '24 03:12 FLYSASA

With orval you can set your own client/fetch in generating options by overriding the mutator being used. For example we use timeout on some default operation ids, and we can add a header on any request/mutation from the orval generated hooks. Both can be set buildtime and used runtime.

Our custom Axios client looks like:

const baseUrl = 'https://foo.test'

type Arg = {
  url: string
  method: AxiosMethod
  params?: { [key: string]: string | string[] | number | boolean | undefined }
  data?: unknown
  headers?: Record<string, string>
  signal?: AbortSignal
  responseType?: string | Blob
}

type Options = {
  headers?: Record<string, string>
  timeout?: number
}

export const client = async <T>(
  { url, method = 'GET', params, data, headers, signal }: Arg,
  options?: Options
): Promise<T> => {
  const urlParams = params
    ? new URLSearchParams(
        Object.keys(params).reduce(
          (acc, key) => (params[key] === undefined ? { ...acc } : { ...acc, [key]: params[key] }),
          {}
        )
      )
    : ''

  return fetchNoCatch({
    url: `${baseUrl}${url}${params ? `?${urlParams}` : ''}`,
    method,
    headers: { ...headers, ...(options?.headers || {}) },
    ...(data ? { body: JSON.stringify(data) } : {}),
    signal,
    timeout: options?.timeout,
  })
}

where fetchNoCatch is just the axios request. Hope this helps.

I now have these options everywhere:

type Options = {
  headers?: Record<string, string>
  timeout?: number
}

Now you can do

useGeneratedHook({ query: {}, request: { headers: {'foo':'bar'}}})

in your Orval config you can also now pass these two Options, for example based on operation id from your open api spec:

override: {
    mutator: {
      path: '../mutator/client.ts',
      name: 'client',
    },
    operations: {
      getFooUnreachable: {
        requestOptions: {
          timeout: 30000,
        },
      },
    },
...

now in the generated hook this will be added standard, same you can do for headers :)

so option to keep it dynamic (consumer hook), or in the already generated version (which you can still overwrite, so can see it as a default... :)).

generated code with orval:

export const getFooUnreachable = (options?: SecondParameter<typeof client>, signal?: AbortSignal) => {
  return client<number[]>(
    { url: `/snip/unreachable`, method: 'GET', signal },
    { timeout: 30000, ...options }
  )
}

examples consumer:

useGetFooUnreachable({ request: { timeout: 10_000 } })
useGetFooUnreachable({ query: { gcTime: 0 }, request: { headers:  { 'foo': 'bar' } })

maapteh avatar Jan 30 '25 17:01 maapteh