redux-toolkit: RTK Query: Difficulties wrangling `content-type: 'multipart/form-data'` headers with form data bodies

I have an api definition based mostly off the examples in the docs, in full below. I have an endpoint I want to add that involves uploading a file to the server. In Axios, the api call looks like this:

      const data = new FormData();
      data.append('image', file);
      await axios.post(URLS.IMAGE(), data, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
      });

This works, despite me not defining a boundary. I don’t know why. edit: It seems axios automatically adds form data boundary: multipart/form-data; boundary=---------------------------35214638641989039512767343688

If I attempt the same thing with an RTK mutation:

    uploadImage: builder.mutation<
      string,
      { payload: FormData}
    >({
      query: ({ payload }) => {
        return {
        url: URLS.IMAGE(),
        method: 'POST',
        body: payload,
        headers: {
          'Content-Type': 'multipart/form-data;',
        },
        };},
    }),

I get this error from Django: Multipart form parse error - Invalid boundary in multipart: None.

While I haven’t guaranteed this issue isn’t due to Django itself, since it works with axios, and not RTK’s baseQuery, I feel I’m doing something wrong with RTK.

I tried removing the headers, and they set a default content-type of application/json, as per the docs: https://redux-toolkit.js.org/rtk-query/api/fetchBaseQuery#using-fetchbasequery . This obviously causes an error from the server.

I tried adding a boundary myself that I found online, a big string like webkit---easdfajf but that was unrecognized as correct by Django - it didn’t parse out the data correctly, it seems, which I believe means I guessed the wrong boundary.

Many solutions online suggest deleting the content-type header and allowing fetch to set it itself when it detects FormData in the body, however this doesn’t seem to be an option with RTK baseQuery.

A potential solution could be to use a queryFn with axios, but I want to avoid that so I can remove axios entirely from my project, and also so I don’t have to handle token setting in some other spot (plus token refresh) arbitrarily.

I could also use queryFn with default fetch api for this one endpoint, but, the issue with token refresh remains, I don’t want to have to re-handle it just for this one endpoint if possible.

Am I missing something in configuration?

Api definition:

const baseQuery = fetchBaseQuery({
  baseUrl: API_URL,
  prepareHeaders: (headers, { getState }) => {
    const token = (getState() as RootState).user.token;
    if (token) {
      headers.set('Authorization', `Bearer ${token}`);
    }

    return headers;
  },
});
const baseQueryWithReauth: BaseQueryFn<
  string | FetchArgs,
  unknown,
  FetchBaseQueryError
> = async (args, api, extraOptions) => {
  await mutex.waitForUnlock();
  const moddedArgs = args;
  if (args?.body) {
    moddedArgs.body = snakeCase(removeNullish(args.body));
  }

  let result = await baseQuery(args, api, extraOptions);
  if (
    result.error &&
    result.error.status === 401 &&
    result.error.data?.code !== 'no_active_account' &&
    result.error.data?.code !== 'not_authenticated'
  ) {
    if (!mutex.isLocked()) {
      const release = await mutex.acquire();
      try {
        const refreshResult = await baseQuery(
          {
            url: URLS.REFRESH_TOKEN,
            method: 'POST',
            body: {
              refresh: (api.getState() as RootState).user.refreshToken,
            },
          },
          api,
          extraOptions
        );
        if (refreshResult?.data?.access) {
          api.dispatch(setToken(refreshResult.data.access as string));

          result = await baseQuery(
            moddedArgs as string | FetchArgs,
            api,
            extraOptions
          );
        } else {
          api.dispatch(setToken(null));
          api.dispatch(setRefreshToken(null));
        }
      } finally {
        release();
      }
    } else {
      await mutex.waitForUnlock();
      result = await baseQuery(
        moddedArgs as string | FetchArgs,
        api,
        extraOptions
      );
    }
  }
  if (result.data) {
    result.data = camelize(result.data);
  }
  if (result.error) {
    result.error = camelize(result.error);
  }
  return result;
};

// Our endpoints are all injected
export const apiSlice = createApi({
  reducerPath: 'api',
  baseQuery: baseQueryWithReauth,
  endpoints: (builder) => ({}),
  tagTypes: [
...
  ],
});

edit3: Setting Content-Type to undefined in headers merely results in the Content-Type being set to application/json

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 16 (5 by maintainers)

Most upvoted comments

Just make sure you are not setting any “Content-type” header for the POST request. This fixed it for me.

You can modify your RTK api as : query: ({ jobId, formData }) => ({ url: your url, method: “POST”, body: formData, formData: true, }),