Bug: codegen-openapi incorrectly handles multipart/form-data requests
OpenAPI spec:
"/api/Avatar": {
"post": {
"tags": [
"Avatar"
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"file": {
"type": "string",
"format": "binary"
}
}
},
"encoding": {
"file": {
"style": "form"
}
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
},
"application/json": {
"schema": {
"type": "string"
}
},
"text/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
When you generate API using above specification and send a request you will see that the request content-type is application/json, but should be multipart/form-data. This obviosly results in 415 (Unsupported Media Type) returned from the server.
Any updates on this?
@Shaker-Pelcro : if there haven't been any comments or releases, there are no updates.
There hasn't been any active work on the codegen stuff in several weeks as far as I know.
If there is no active development on the codegen, what people are using instead? Are there any reliable tools for generating TypeScript code from OpenAPI spec, or do people implement client code by hand?
@ajaskiewiczpl : to be clear, the current codegen packages work in general. "No active development" does not mean "this code does not work at all" - it just means "the current code exists as-is, and no one is specifically trying to fix bugs or add new features right this second".
But, there's only a couple of active Redux maintainers, and Lenz is the only one who's worked on the codegen packages. So, no ETA on when we will have time to try to make changes to those packages.
I totally get it. I don't blame you, I just wanted to know what are the alternatives, or more specifically - how people deal with generating OpenAPI clients in general.
Yeah, I know about the situation with multiple codegen PRs being open and unmerged, and I'm really sorry for that. I just switched jobs and need to settle in right now. I do plan to tackle those, but it could still be a few weeks until I get to it.
The plan is to not necessarily add own code, but at least get through the PRs that will be open at that time, get them merged and cut a new release. So now would be a good moment to open PRs for outstanding bugs ;)
The only issue I face with codegen is that for this endpoint:
/license/import:
post:
operationId: applyLicense
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
properties:
license:
type: string
format: binary
generates this:
export type ImportAppApiArg = { config?: Blob; }; // <-- This is not the correct type
const injectedRtkApi = api
.injectEndpoints({
endpoints: (build) => ({
importApp: build.mutation<ImportAppApiResponse, ImportAppApiArg>({
query: (queryArg) => ({
url: `/applications/import`,
method: 'POST',
body: queryArg,
}),
}),
}),
overrideExisting: false,
});
The result is this typescript error:
The error goes away when I manually update ImportAppApiArg.
export type ImportAppApiArg = FormData;
Any update on this? My company is currently facing the exact same issue, which is a shame since this workflow works very well for any other use case.
I often enhance the generated API that must use Contet-Type: multipart/form-data as follows.
{
"openapi": "3.0.1",
"info": {
"title": "OpenAPI definition",
"version": "v0"
},
"servers": [
{
"url": "http://localhost:8080/",
"description": "Generated server url"
}
],
"paths": {
"/api/Avatar": {
"post": {
"tags": ["Avatar"],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"required": ["file"],
"type": "object",
"properties": {
"file": {
"type": "string",
"format": "binary"
}
}
},
"encoding": {
"file": {
"style": "form"
}
}
}
}
},
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
},
"application/json": {
"schema": {
"type": "string"
}
},
"text/json": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
}
}
Generated:
.injectEndpoints({
endpoints: (build) => ({
postApiAvatar: build.mutation<
PostApiAvatarApiResponse,
PostApiAvatarApiArg
>({
query: (queryArg) => ({
url: `/api/Avatar`,
method: 'POST',
body: queryArg.body,
}),
invalidatesTags: ['Avatar'],
}),
}),
Enhanced:
.enhanceEndpoints({
endpoints: {
postApiAvatar: {
query: (arg) => {
const formData = new FormData();
formData.append('file', arg.body.file);
return {
url: `/api/Avatar`,
method: 'POST',
body: formData,
};
},
},
},
})
However, I believe this should be a temporary measure.
I hope that codegen-openapi will support multipart/form-data one day soon.
I'm using rtk-query-openapi to generate API hooks. but the generated query doesn't contain formData:true. Is there any specific method to add this flag to file upload query by changing my configurations?.
So Since no efforts has been made in this so far (No pressure on maintainers, I understand how hard it is to do this, and for free nonetheless, thank you!)
I've had a solution for this and I want to share it in case it can help anyone or spark a conversation that could improve it.
Preface: I made it a standard in my code base that all generated API files must have an enhanced API (using enhanceEndpoints), most people using the code-gen should have that already as it's usually needed for providing/invalidating tags.
Below is a snippet from our application that handles file uploads.
export default generatedAiContentCoderApi.enhanceEndpoints({
endpoints: {
aiContentCoderResponsesUploadFileCreate: {
query: ({ uploadFileCreate }) => {
const formData = new FormData();
formData.append("file", uploadFileCreate.file);
return {
url: "/api/v1/ai_content_coder/responses/upload_file/",
method: "POST",
body: formData,
formData: true,
};
},
invalidatesTags: [],
},
},
});
now what I did is that I use the generated type as it is, and in my enhanced API I override query to create my FormData using the generated type and use that in the query.
This is, in my opinion, much better than overriding the generated code directly as it won't be overridden when you update the API unlike the other solution.
Still, it's not exactly ideal, maintainability is not great:
- updating the URL or method will break this but that should be rather infrequent;
- more importantly, when updating the data type (
uploadFileCreatein the snippet) we will also need to edit the enhanced API in order to add the new data to theformData, and missing this can be a pitfall.
Let me know your thoughts!
This helped me for graphql
https://github.com/jasonkuhrt/graphql-request/discussions/650
@Moe-Hassan-123 and how do you use that enhance api? do you need to export hook from your custom code?
@Moe-Hassan-123 I manage to get without enchanced endpoints. Just create FormData and use it instead generated request object:
function handleSubmitImportCsv(e: FormEvent) {
e.preventDefault();
const formData = new FormData();
formData.append("city", "Chicago");
formData.append("industry", "Marketing");
formData.append("csv_file", csv_file);
importCsv({
prospectsImportCsvRequest: formData,
});
}
.
typescript will complain:
Type 'FormData' is missing the following properties from type 'ProspectsImportCsvRequest': csv_file, city, industry
Not sure if anyone is interested, but I use a helper function to ensure type safety. It is a bit react-native specific because of the way Blob values are handled, but generally it should work in any environment.
Helper:
executeMultipartFormDataMutation
interface ArgsBase {
body: Record<string, unknown>
[key: string]: unknown
}
type TransformArgs<T> = {
[P in keyof T]: P extends 'body'
? {
[P2 in keyof T[P]]: T[P][P2] extends Blob
? { uri: string; type: string; name: string }
: T[P][P2]
}
: T[P]
}
/**
* RTK Query OpenAPI codegen doesn't support typing for multipart/form-data requests. This helper function takes the generated mutation
* function signature and properly structures the request
* @see https://github.com/reduxjs/redux-toolkit/issues/1827
*/
export const executeMultipartFormDataMutation = <Args extends ArgsBase, R>(
fn: (args: Args) => R,
args: TransformArgs<Args>
): R => {
const form = new FormData()
Object.entries(args.body).forEach(([k, v]: [string, unknown]) => {
let value
switch (typeof v) {
case 'object':
if (v === null) {
value = ''
} else if ('uri' in v) {
// react-native specific local file handling
value = v as unknown as Blob
} else {
value = JSON.stringify(v)
}
break
case 'string':
case 'bigint':
case 'boolean':
case 'function':
case 'number':
case 'symbol':
value = v.toString()
break
case 'undefined':
default:
value = ''
break
}
form.append(k, value)
})
// @ts-expect-error
return fn({
...args,
body: form,
})
}
Usage:
dispatch(
executeMultipartFormDataMutation(myApi.endpoints.myEndpoint.initiate, {
body: {
client_id: clientId,
token: token.access_token,
},
})
@andrejpavlovic what is dispatch is that redux-toolkit dispatch function? You gave example with string, but it works with files right?
ok, now the simplest solution I've got so far:
to get this submit code:
function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
// @ts-expect-error: rtk query gen
importCsv({ prospectsImportCsvRequest: formData });
}
The current solution I came up with is to rewrite the query manually for routes which require formData by enhancing the endpoints of the generated Api. It's not the most practical, but it did integrate pretty well in my workflow. Here's a rough explanation of how this works:
import { LoginLoginPostApiArg, generatedApi } from '@/api/generated/Api';
const enhancedApi = generatedApi.enhanceEndpoints({
endpoints: {
// The rest of your endpoints should also be enhanced here
loginLoginPost: {
query: (queryArg: LoginLoginPostApiArg) => ({
url: `/login`,
method: "POST",
body: new URLSearchParams(queryArg.bodyLoginLoginPost as unknown as string),
formData: true,
}),
},
}
});
export default enhancedApi;
I don't consider it to be most elegant but I'd say it does the job, is type safe, and doesn't require you to wait for the developers to implement this.
The current solution I came up with is to rewrite the query manually for routes which require formData by enhancing the endpoints of the generated Api. It's not the most practical, but it did integrate pretty well in my workflow. Here's a rough explanation of how this works:
import { LoginLoginPostApiArg, generatedApi } from '@/api/generated/Api'; const enhancedApi = generatedApi.enhanceEndpoints({ endpoints: { // The rest of your endpoints should also be enhanced here loginLoginPost: { query: (queryArg: LoginLoginPostApiArg) => ({ url: `/login`, method: "POST", body: new URLSearchParams(queryArg.bodyLoginLoginPost as unknown as string), formData: true, }), }, } }); export default enhancedApi;I don't consider it to be most elegant but I'd say it does the job, is type safe, and doesn't require you to wait for the developers to implement this.
this works for text, but fail when used with file / bytes / blob
The current solution I came up with is to rewrite the query manually for routes which require formData by enhancing the endpoints of the generated Api. It's not the most practical, but it did integrate pretty well in my workflow. Here's a rough explanation of how this works:
import { LoginLoginPostApiArg, generatedApi } from '@/api/generated/Api'; const enhancedApi = generatedApi.enhanceEndpoints({ endpoints: { // The rest of your endpoints should also be enhanced here loginLoginPost: { query: (queryArg: LoginLoginPostApiArg) => ({ url: `/login`, method: "POST", body: new URLSearchParams(queryArg.bodyLoginLoginPost as unknown as string), formData: true, }), }, } }); export default enhancedApi;I don't consider it to be most elegant but I'd say it does the job, is type safe, and doesn't require you to wait for the developers to implement this.
this works for text, but fail when used with file / bytes / blob
Totally, yes, I forgot to mention that, my use case was plain text
createApiDocumentsPost: build.mutation<
CreateApiDocumentsPostApiResponse,
CreateApiDocumentsPostApiArg
>({
query: (queryArg) => {
const parsedBody = new FormData();
for (const [key, val] of Object.entries(
queryArg.createPublicDocumentInput,
)) {
parsedBody.append(key, val);
}
return {
url: `/api/documents`,
method: "POST",
body: parsedBody,
};
},
invalidatesTags: ["documents"],
}),
another solution