hey-api / openapi-ts

🚀 The OpenAPI to TypeScript codegen. Generate clients, SDKs, validators, and more. Support: @mrlubos
https://heyapi.dev
Other
1.39k stars 107 forks source link

Option to ignore headers #1020

Open juliusfriedman opened 2 months ago

juliusfriedman commented 2 months ago

Description

Just like in openapi-generator for java it would be nice if there was an option to ignore header parameters e.g.

https://github.com/OpenAPITools/openapi-generator/blob/master/docs/generators/spring.md

The reason for this is primarily that we have a headerResolver which add the required headers but when using the client generated from this package the header cannot be easily changed.

The problem lies here: https://github.com/openapi-ts/openapi-typescript/blob/main/packages/openapi-typescript/src/transform/header-object.ts

It would be beneficial if there was an option to ignore headers or have options similar to the open api generator cited above.

Please let me know if further information is required.

mrlubos commented 2 months ago

Hey @juliusfriedman, I've heard this come up a few times. Are you able to share a (pseudo) code that demonstrates how you'd like to be able to use the generated client that you can't do today?

juliusfriedman commented 2 months ago

I'm away from keyboard right now, but the root of the issue is that I don't want the header to be a parameter to the method that is generated.

I would only like the header to appear for use on the swagger/open api interface.

I would like to use a headerResolver which depends on the method being fetched which header will be sent.

This implies I can't have the header name hard-coded in the client which is generated nor can I can have the headers become parameters of the method.

I understand for some people it's good BUT if there was an option to ignore headers it would be better.

Right now I work around this by generating 2 different openApi documents... one with the header as a parameter and the other without.

Hopefully this helps, I will try to add more code when I get back to my pc.

Thanks

juliusfriedman commented 2 months ago

service.someMethod() is generated when the header is ignored lets say and service.someMethod(requiredParamter) is generated when there is a header, what's even worse is that when the header is inserted its based on the time the client was generated so if the header changes at runtime then this will render the client useless.

Please let me know if I can explain any better.

mrlubos commented 2 months ago

Can you share a spec that results in this type of useless client?

juliusfriedman commented 2 months ago

Here is the spec of the method

"/api/ForgotPassword/SendEmail/{cultureName}": {
      "post": {
        "tags": [
          "ForgotPassword"
        ],
        "operationId": "ForgotPassword_SendEmail",
        "parameters": [
          {
            "name": "cultureName",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "nullable": true
            },
            "x-position": 1
          },
          {
            "type": "string",
            "name": "__RequestVerificationToken",
            "in": "header",
            "required": true,
            "description": "Anti Forgery Token",
            "schema": {
              "type": "string",
              "format": "string"
            }
          }
        ],
        "requestBody": {
          "x-name": "request",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendForgotPasswordEmailRequest"
              }
            }
          },
          "required": true,
          "x-position": 2
        },
        "responses": {
          "200": {
            "description": ""
          }
        }
      }
    },

Here is a method from a generated client which becomes useless

public static forgotPasswordSendEmail({
    cultureName,
    requestVerificationToken,
    requestBody,
  }: {
    cultureName: string | null,
    /**
     * Anti Forgery Token
     */
    requestVerificationToken: string,
    requestBody: SendForgotPasswordEmailRequest,
  }): CancelablePromise<any> {
    return __request(OpenAPI, {
      method: 'POST',
      url: '/api/ForgotPassword/SendEmail/{cultureName}',
      path: {
        'cultureName': cultureName,
      },
      headers: {
        '__RequestVerificationToken': requestVerificationToken,
      },
      body: requestBody,
      mediaType: 'application/json',
    });
  }

This works until the header name changes for the Anti Forgery Token i.e. -> __RequestVerificationToken can change arbitrarily with user configuration and we would have to regenerate the client every time where as with a headerResolver this doesn't happen and the header name is dynamically assigned along with the token.

mrlubos commented 2 months ago

And how do you know what's the new value of that header?

juliusfriedman commented 2 months ago

It depends on the tech stack in use but at a high level, during runtime we could resolve the header from a separate GET request dynamically, this GET request returns the header name and token and we use it on subsequent requests and is why the headerResolver approach works for us.

mrlubos commented 2 months ago

Can you explain why is __RequestVerificationToken required if it is NOT the header you're expecting? Is this an ASP.NET application? I'm trying to find a documentation that describes your behaviour. Right now I don't understand why the OpenAPI spec would define this header as required if you know it will not exist?

juliusfriedman commented 2 months ago

Yes and Yes,

We add it as a required header for the swagger operations so that users can use the swagger interface to test the API without the UI portion of the application.

The header will exist but maybe named something differently by the time the application is running (which is after the client is compiled)

The way we have worked around this is with a headerResolver which generates the call to get the required headerName and token when making requests.

The way we ended up using the client was that we needed to generate a version of the openApi spec WITHTOUT the required parameter which is why I made this issue.

If there was a way to ignore the header it would be the same as me omitting it from the API Spec but only for the open api json.

Hopefully that makes sense :)

mrlubos commented 2 months ago

Okay, do you think an option along the lines of shouldProcessHeader() would work? Would you be looking to use the package in a programmatic way through Node.js?

juliusfriedman commented 2 months ago

Yes, an option like that should definitely work, Just as a FYI we are actually using https://github.com/ferdikoomen/openapi-typescript-codegen which I believe is the prior version of this library.

I went to update to this library but then when I saw this library has the same issue I stopped what I was doing and instead created an issue.

Once we have the ability to skip the header either by command line or code I can experiment and see where its most useful, I believe the command line would be the most useful as of right now.

Thank you and please let me know if you need anything else!

karolis-kniuksta-trimble commented 3 weeks ago

@juliusfriedman did you come up with some workaround for this?

juliusfriedman commented 3 weeks ago

Yes, I opted to change the OpenApi data we generate the client from to NOT include the required header for now.

I essentially generate 2 different forms of OpenApi data:

1) Used for compilation / generation of the TypeScript client, in this rendition the required header is removed 2) Used for runtime, e.g. by users downloading the definition, in this rendition the required header is specified

1) allows for generating the client and 2) allows for uses within OpenApi interfaces e.g. swagger.

The generated client is then augmented with a HEADERS function which allows resolving the headers at each request.

The library I was using was openapi-typescript-codegen which seems to have a slightly different paradigm then this library when it comes to headers resolving, I am not sure this library supports such a concept (hence why I made this issue).

karolis-kniuksta-trimble commented 2 weeks ago

Yes, I opted to change the OpenApi data we generate the client from to NOT include the required header for now.

I essentially generate 2 different forms of OpenApi data:

  1. Used for compilation / generation of the TypeScript client, in this rendition the required header is removed
  2. Used for runtime, e.g. by users downloading the definition, in this rendition the required header is specified

1) allows for generating the client and 2) allows for uses within OpenApi interfaces e.g. swagger.

The generated client is then augmented with a HEADERS function which allows resolving the headers at each request.

The library I was using was openapi-typescript-codegen which seems to have a slightly different paradigm then this library when it comes to headers resolving, I am not sure this library supports such a concept (hence why I made this issue).

Thanks. As we can't change APIs we set headers to null + //@ts-ignore and set headers in interceptors, where it belongs.

ha1fstack commented 2 weeks ago

Altering the input openapi schema instead of modifying output was much simpler for my case.

I used neotraverse to modify the fetched json schema and it worked great.

const rawSchema = await fetch(openapiSchema).then(res => res.json());

const transformedSchema = new Traverse(rawSchema).forEach(ctx => {
  if (ctx.path.at(-1) === 'parameters') {
    ctx.update(
      ctx.node.filter((node: any) => {
        if (node.in === 'header' && node.name === 'X-Foo-Header') {
          return false;
        }
        if (node.in === 'header' && node.name === 'X-Bar-Header') {
          return false;
        }
        return true;
      })
    );
  }
});
return defineConfig({
  client: '@hey-api/client-fetch',
  input: transformedSchema,
  ...
})
ha1fstack commented 2 weeks ago

+ In nodejs environment, what about providing middleware for schema before generation to suit more needs instead of just ignoring headers?

karolis-kniuksta-trimble commented 2 weeks ago

Altering the input openapi schema instead of modifying output was much simpler for my case.

I used neotraverse to modify the fetched json schema and it worked great.

const rawSchema = await fetch(openapiSchema).then(res => res.json());

const transformedSchema = new Traverse(rawSchema).forEach(ctx => {
  if (ctx.path.at(-1) === 'parameters') {
    ctx.update(
      ctx.node.filter((node: any) => {
        if (node.in === 'header' && node.name === 'X-Foo-Header') {
          return false;
        }
        if (node.in === 'header' && node.name === 'X-Bar-Header') {
          return false;
        }
        return true;
      })
    );
  }
});
return defineConfig({
  client: '@hey-api/client-fetch',
  input: transformedSchema,
  ...
})

This looks very useful. Thanks for sharing!

pappebury commented 4 days ago

To add an alternative version using only javascript and making the headers optional instead of removing them I modified @ha1fstack answer to this:

const traverseAndModify = (obj: any) => {
  if (Array.isArray(obj)) {
    return obj.map((node) => {
      if (node.in === "header" && node.name === "Authorization") {
        return { ...node, required: false }
      }
      return node
    })
  }

  if (typeof obj === "object" && obj !== null) {
    for (const key in obj) {
      obj[key] = traverseAndModify(obj[key])
    }
  }

  return obj
}

const transformedSchema = traverseAndModify(rawSchema)