swagger-typescript-api icon indicating copy to clipboard operation
swagger-typescript-api copied to clipboard

Axios client does not respect `type` param for `Content-Type` header

Open Jackman3005 opened this issue 1 year ago • 1 comments

Problem

When generating an Axios client, the Content-Type headers are not being set correctly if using the default type param to specify the content type (note: if you specify specifically the content type header in the param, that will work).

Background

The generated code results in something like this:

this.request<ExecuteScriptData, any>({
      path: `/execute`,
      method: "POST",
      body: data,
      secure: true,
      type: ContentType.Json,
      ...params,
    });

Although you can see it is setting type: ContentType.Json. This is not having the desired effect of setting the Content-Type in the header of the request.

From what I can tell, this was a bit of an oversight in how we merge params and the difference in "defaults" between fetch and axios.

Axios defaults include header values such as 'Content-Type' whereas fetch defaults do not.

Defaults

Axios defaults

{
  transitional: {
    silentJSONParsing: true,
    forcedJSONParsing: true,
    clarifyTimeoutError: false
  },
  adapter: [Function: httpAdapter],
  transformRequest: [ [Function: transformRequest] ],
  transformResponse: [ [Function: transformResponse] ],
  timeout: 0,
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',
  maxContentLength: -1,
  maxBodyLength: -1,
  env: {
    FormData: [Function: FormData] {
      LINE_BREAK: '\r\n',
      DEFAULT_CONTENT_TYPE: 'application/octet-stream'
    }
  },
  validateStatus: [Function: validateStatus],
  headers: {
    common: { Accept: 'application/json, text/plain, */*' },
    delete: {},
    get: {},
    head: {},
    post: { 'Content-Type': 'application/x-www-form-urlencoded' },
    put: { 'Content-Type': 'application/x-www-form-urlencoded' },
    patch: { 'Content-Type': 'application/x-www-form-urlencoded' }
  },
  baseURL: 'https://api.scriptable.run/v1'
}

Fetch defaults

{
    credentials: 'same-origin',
    headers: {},
    redirect: 'follow',
    referrerPolicy: 'no-referrer',
}

Generated request function


  public request = async <T = any, _E = any>({
    secure,
    path,
    type,
    query,
    format,
    body,
    ...params
  }: FullRequestParams): Promise<AxiosResponse<T>> => {
    const secureParams =
      ((typeof secure === "boolean" ? secure : this.secure) &&
        this.securityWorker &&
        (await this.securityWorker(this.securityData))) ||
      {};
    const requestParams = this.mergeRequestParams(params, secureParams);
    const responseFormat = format || this.format || undefined;

    if (type === ContentType.FormData && body && body !== null && typeof body === "object") {
      body = this.createFormData(body as Record<string, unknown>);
    }

    return this.instance.request({
      ...requestParams,
      headers: {
        ...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
        ...(requestParams.headers || {}),
      },
      params: query,
      responseType: responseFormat,
      data: body,
      url: path,
    });
  };

Generated mergeRequestParams

  protected mergeRequestParams(params1: AxiosRequestConfig, params2?: AxiosRequestConfig): AxiosRequestConfig {
    const method = params1.method || (params2 && params2.method);

    return {
      ...this.instance.defaults,
      ...params1,
      ...(params2 || {}),
      headers: {
        ...((method && this.instance.defaults.headers[method.toLowerCase() as keyof HeadersDefaults]) || {}),
        ...(params1.headers || {}),
        ...((params2 && params2.headers) || {}),
      },
    };
  }

Possible Solution

Remove the following line from the headers field in mergeRequestParams return value.

...((method && this.instance.defaults.headers[method.toLowerCase() as keyof HeadersDefaults]) || {}),

Move that line to the request function so that it ends up building headers like this:

headers: {
  ...((requestParams.method && this.instance.defaults.headers[requestParams.method.toLowerCase() as keyof HeadersDefaults]) || {}),
  ...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
  ...(requestParams.headers || {}),
},

Jackman3005 avatar Sep 26 '22 06:09 Jackman3005

The other solution that came to mind was to just swap the order of the lines in headers to be:

headers: {
  ...(requestParams.headers || {}),
  ...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}),
},

But this would likely be very confusing for anyone that was explicitly setting the Content-Type header via params, since the type would override that.

Jackman3005 avatar Sep 26 '22 06:09 Jackman3005

mergeRequestParams should not work with this.instance.defaults (or this.instance.defaults.headers) at all (as I already mentioned in #331) - there is no reason for that. Axios defaults are it's own - Axios merges them into request config automatically.

I'm using forked version of the template with following code and it works as expected:

protected mergeRequestParams(params1: AxiosRequestConfig, params2?: AxiosRequestConfig): AxiosRequestConfig {
      const method = params1.method || (params2 && params2.method)

        return {
            ...params1,
            ...(params2 || {}),
            headers: {
                ...(params1.headers || {}),
                ...((params2 && params2.headers) || {}),
            },
        };
    }

Liwoj avatar Oct 20 '22 04:10 Liwoj

@Jackman3005 @Liwoj this problem should be fixed in 11.* releases, thanks @Liwoj for this request changes

js2me avatar Nov 05 '22 18:11 js2me