redux-toolkit
redux-toolkit copied to clipboard
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
Before we go deeper - fetchBaseQuery just uses fetch internally. Do you get this to work with fetch?
Before we go deeper - fetchBaseQuery just uses
fetchinternally. Do you get this to work withfetch?
Yes, and without setting content type header, fetch automatically set the header correctly:
multipart/form-data; boundary=----WebKitFormBoundaryY2zumZlB8OHh3bBN
Here's what the queryFn looks like:
uploadLogo: builder.mutation<
string,
{ payload: FormData }
>({
queryFn: async ({ payload }) => {
const result = await fetch(URLS.IMAGE(), {
method: 'POST',
body: payload,
headers: {
'Authorization': `Bearer ${localStorage.getItem('accessToken')}`,
},
})
const data = await result.json();
return { data };
},
}),
This is the code in the current fetchBaseQuery that should only set content-type for JSON content if it is a plain object:
// Only set the content-type to json if appropriate. Will not be true for FormData, ArrayBuffer, Blob, etc.
const isJsonifiable = (body: any) =>
typeof body === 'object' &&
(isPlainObject(body) ||
Array.isArray(body) ||
typeof body.toJSON === 'function')
if (!config.headers.has('content-type') && isJsonifiable(body)) {
config.headers.set('content-type', jsonContentType)
}
Are you sure that you are on the latest version of RTK? I remember that was changed a while ago.
Ah, that's a good point, I should have checked that.
While our package.json specifies an older version, it appears, according to yarn.lock, that we have the latest version:
"@reduxjs/toolkit@^1.7.2":
version "1.8.5"
resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.8.5.tgz#c14bece03ee08be88467f22dc0ecf9cf875527cd"
integrity sha512-f4D5EXO7A7Xq35T0zRbWq5kJQyXzzscnHKmjnu2+37B3rwHU6mX9PYlbfXdnxcY6P/7zfmjhgan0Z+yuOfeBmA==
dependencies:
immer "^9.0.7"
redux "^4.1.2"
redux-thunk "^2.4.1"
reselect "^4.1.5"
Is my reading of our yarn.lock correct?
But, it sounds like thus something, somewhere, in my config must be setting a content-type header! I'll hunt again
Edit: I searched everywhere, with full project greps as well, and found nothing! very weird. I'll try stepping through with a debugger and see when the header gets set
Edit: as an afterthought, I made sure isJsonifiable returns false for my payload, and it does.
Can you try the current 1.9 alpha? I'm at this point not sure if this could be a new change.
I'm on version 1.9.1, and I'm trying to send an image with a mutation request, but the "content-type" in the request header is always set to "application/json", If I do set it manually in the mutation to "multipart/form-data", it doesn't set the boundaries, and my request is not successful.
@Shaker-Pelcro does the exact same call with fetch work? RTK Query just calls fetch in the end.
@phryneas
Yes, it works fine with fetch and the content-type is set properly without me setting it manually at all.
@Shaker-Pelcro the question is less about the content-type, but about the boundaries. What are you passing in there?
I don't see any repros attached to this issue, so I'm going to close it. If someone can provide a repo or CodeSandbox that shows this happening, we might be able to take a look.
You can modify your RTK api as : query: ({ jobId, formData }) => ({
url: your url,
method: "POST",
body: formData,
formData: true,
}),
Just make sure you are not setting any "Content-type" header for the POST request. This fixed it for me.
add formData : true
@MayorErnest thank you, it helped. I don't know why it is almost everywhere stated to use 'Content-Type': 'multipart/form-data' 😣
Thank you @poojavirgo and @MayorErnest for providing the solution. It was really helpful.
just remove the header for POST req
Just make sure you are not setting any "Content-type" header for the POST request. This fixed it for me.
Thank you it worked for me don't know why there are a lot of people adding 'Content-Type': 'multipart/form-data' in header :(