Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: codegen-openapi incorrectly handles multipart/form-data requests #3063

Open
ajaskiewiczpl opened this issue Jan 6, 2023 · 17 comments
Open

Comments

@ajaskiewiczpl
Copy link

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.

@Shaker-Pelcro
Copy link

Any updates on this?

@markerikson
Copy link
Collaborator

@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.

@ajaskiewiczpl
Copy link
Author

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?

@markerikson
Copy link
Collaborator

@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.

@ajaskiewiczpl
Copy link
Author

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.

@phryneas
Copy link
Member

phryneas commented Feb 6, 2023

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 ;)

@dzegarra
Copy link

dzegarra commented May 22, 2023

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:
image

The error goes away when I manually update ImportAppApiArg.

export type ImportAppApiArg = FormData;

@BSoDium
Copy link

BSoDium commented Jul 25, 2023

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.

@yukiyokotani
Copy link

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.

@nipunravisara
Copy link

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?.

@Moe-Hassan-123
Copy link

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 (uploadFileCreate in the snippet) we will also need to edit the enhanced API in order to add the new data to the formData, and missing this can be a pitfall.

Let me know your thoughts!

@joetristar
Copy link

This helped me for graphql

jasonkuhrt/graphql-request#650

@tombohub
Copy link

@Moe-Hassan-123 and how do you use that enhance api? do you need to export hook from your custom code?

@tombohub
Copy link

tombohub commented Apr 19, 2024

@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

@andrejpavlovic
Copy link
Contributor

andrejpavlovic commented Apr 19, 2024

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,
    },
  })

@tombohub
Copy link

@andrejpavlovic what is dispatch is that redux-toolkit dispatch function? You gave example with string, but it works with files right?

@tombohub
Copy link

tombohub commented Apr 21, 2024

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 });
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests