Closed14

OpenAPI Generatorでエラーレスポンスって拾えないのかな?かな?

takky94takky94

OpenAPI GeneratorでTS用の型生成とfetchをラップしたクラスを生成してる

  1. yamlをゴリゴリ書く
  2. openapi-generatorコマンド叩く
  3. 型とfetchのラップクラス生成される
  4. APIとのやりとりおよびそのResponse/Requestの型は生成物を使用

こうしてたんだけど
oneOfが使えなかったりqueryパラメータでオブジェクトを指定してもキーバリューがうまく扱えてなかったり、その他にもうんざりするような未対応事項が色々あって、ちょっとパッチ当てるくらいなら頑張ってみようかなと思ったけど他のツールとも依存関係にありすぎてお気持ちポッキリ折られた

そんで今日改めて認知してたけど自分の中でStay状態にしてたエラーレスポンスを拾えてない件を指摘され、「使わないようにする」という手も視野に入れつつ、エラーレスポンスを拾えないか模索してみんとてするなり

takky94takky94

思い出してきた

OpenAPI Generatorの生成物の中で、BaseAPIというクラスがあり、こいつを呼んでゴニョる

/**
 * This is the base class for all generated API classes.
 */
export class BaseAPI {

    private middleware: Middleware[];

    constructor(protected configuration = new Configuration()) {
        this.middleware = configuration.middleware;
    }

    withMiddleware<T extends BaseAPI>(this: T, ...middlewares: Middleware[]) {
        const next = this.clone<T>();
        next.middleware = next.middleware.concat(...middlewares);
        return next;
    }

    withPreMiddleware<T extends BaseAPI>(this: T, ...preMiddlewares: Array<Middleware['pre']>) {
        const middlewares = preMiddlewares.map((pre) => ({ pre }));
        return this.withMiddleware<T>(...middlewares);
    }

    withPostMiddleware<T extends BaseAPI>(this: T, ...postMiddlewares: Array<Middleware['post']>) {
        const middlewares = postMiddlewares.map((post) => ({ post }));
        return this.withMiddleware<T>(...middlewares);
    }

    protected async request(context: RequestOpts, initOverrides?: RequestInit): Promise<Response> {
        const { url, init } = this.createFetchParams(context, initOverrides);
        const response = await this.fetchApi(url, init);
        if (response.status >= 200 && response.status < 300) {
            return response;
        }
        throw response;
    }

    private createFetchParams(context: RequestOpts, initOverrides?: RequestInit) {
        let url = this.configuration.basePath + context.path;
        if (context.query !== undefined && Object.keys(context.query).length !== 0) {
            // only add the querystring to the URL if there are query parameters.
            // this is done to avoid urls ending with a "?" character which buggy webservers
            // do not handle correctly sometimes.
            url += '?' + this.configuration.queryParamsStringify(context.query);
        }
        const body = ((typeof FormData !== "undefined" && context.body instanceof FormData) || context.body instanceof URLSearchParams || isBlob(context.body))
        ? context.body
        : JSON.stringify(context.body);

        const headers = Object.assign({}, this.configuration.headers, context.headers);
        const init = {
            method: context.method,
            headers: headers,
            body,
            credentials: this.configuration.credentials,
            ...initOverrides
        };
        return { url, init };
    }

    private fetchApi = async (url: string, init: RequestInit) => {
        let fetchParams = { url, init };
        for (const middleware of this.middleware) {
            if (middleware.pre) {
                fetchParams = await middleware.pre({
                    fetch: this.fetchApi,
                    ...fetchParams,
                }) || fetchParams;
            }
        }
        let response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init);
        for (const middleware of this.middleware) {
            if (middleware.post) {
                response = await middleware.post({
                    fetch: this.fetchApi,
                    url: fetchParams.url,
                    init: fetchParams.init,
                    response: response.clone(),
                }) || response;
            }
        }
        return response;
    }

    /**
     * Create a shallow clone of `this` by constructing a new instance
     * and then shallow cloning data members.
     */
    private clone<T extends BaseAPI>(this: T): T {
        const constructor = this.constructor as any;
        const next = new constructor(this.configuration);
        next.middleware = this.middleware.slice();
        return next;
    }
};
takky94takky94

で、👆 上のコード内、 👇 下記部分でステータスコードが200以上300未満だったらそのまま打ち返し、それ以外だったらthrowしてる
んで、確か呼び出し元ではcatchしたらそのエラーオブジェクトを握り潰してた気がすんだよなぁ

    protected async request(context: RequestOpts, initOverrides?: RequestInit): Promise<Response> {
        const { url, init } = this.createFetchParams(context, initOverrides);
        const response = await this.fetchApi(url, init);
        if (response.status >= 200 && response.status < 300) {
            return response;
        }
        throw response;
    }
takky94takky94

呼び出し元ではBaseAPIを継承して、その中で各エンヨポインヨごとの関数作ってる
その関数内で 👆 のrequest()呼んでて、そいつを引数として今度はBaseAPIJSONApiResponse()に投げてる

import * as runtime from '../runtime' // 👈 BaseAPI

async hogeIdPatchRaw(requestParameters: HogeIdPatchOperationRequest, initOverrides?: RequestInit): Promise<runtime.ApiResponse<HogeIdPatchResponse>> {
  // ...
  const response = await this.request({
      path: `/hoges/{id}`.replace(`{${"id"}}`, encodeURIComponent(String(requestParameters.id))),
      method: 'PATCH',
      headers: headerParameters,
      query: queryParameters,
      body: HogesIdPatchRequestToJSON(requestParameters.hogesIdPatchRequest),
  }, initOverrides);

  return new runtime.JSONApiResponse(response, (jsonValue) => HogesIdPatchResponseFromJSON(jsonValue));
takky94takky94

あー、JSONAPI~では特に何もしてなくてjsonをパースしてる

export class JSONApiResponse<T> {
    constructor(public raw: Response, private transformer: ResponseTransformer<T> = (jsonValue: any) => jsonValue) {}

    async value(): Promise<T> {
        return this.transformer(await this.raw.json());
    }
}

呼び出し元で第二引数としてしれっと渡してたHogesIdPatchResponseFromJSONが実行されてると

  return new runtime.JSONApiResponse(response, (jsonValue) => HogesIdPatchResponseFromJSON(jsonValue));
takky94takky94

こいつもただのラッパーだった

export function HogeIdPatchResponseFromJSON(json: any): HogesIdPatchResponse {
    return HogesIdPatchResponseFromJSONTyped(json, false);
}

HogesIdPatchResponseFromJSONTypedを見ると

export function HogesIdPatchResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): HogesIdPatchResponse {
    if ((json === undefined) || (json === null)) {
        return json;
    }
    return {
        
        'data': !exists(json, 'data') ? undefined : HogeFromJSON(json['data']),
    };
}

あ、こいつだ
エラーを握り潰してはないっぽい
yamlで定義した200のレスポンスの第一階層のキーが存在するかどうかでresponseを返すかundefinedを返すかということをやっていて、200以外のレスポンスの第一階層のキーでも判別したい

takky94takky94

ざわ……散々OpenAPI Generatorに不満をぶつけまくった挙句、自分が悪かった可能性が濃厚になってきた……いや、僕は悪くない……

takky94takky94

yamlでは、エラー関係はコンポーネント化してる

failed:
  description: 失敗
  content:
    application/json:
      schema:
        title: "PostOrPatchOrDeleteErrorObj"
        type: "object"
        properties:
          code:
            $ref: "../schemas/common/index.yml#/code"
          message:
            $ref: "../schemas/common/index.yml#/message"
          description:
            $ref: "../schemas/common/index.yml#/description"
          token:
            $ref: "../schemas/common/index.yml#/token"
        required:
          - "code"
          - "message"
          - "description"

そんで、こいつを各種APIのresponse内で呼び出してる

  responses:
    "200":
      description: 成功
      content:
        application/json:
          schema:
            title: "HogesIdPatchResponse"
            type: "object"
            properties:
              data:
                $ref: "../schemas/index.yml#/HogeResponse"
            required:
              - "data"
    "400":
      $ref: "../responses/index.yml#/failed"
    "401":
      $ref: "../responses/index.yml#/failed"
    "404":
      $ref: "../responses/index.yml#/failed"
    "500":
      $ref: "../responses/index.yml#/failed"

で、そのエラーレスポンスは、PostOrPatchOrDeleteErrorObjという型になってるけど、こいつがどこからも呼び出されてない

takky94takky94

あれ、てか普通にエラーオブジェクト潰してなかったから呼び出しもとでcatchできるのか

takky94takky94

例えば

    content:
      application/json:
        schema:
          title: "hogehogeGetResponse"
          properties:
            data:
              type: "array"
              items:
                oneOf:
                  - $ref: "../schemas/hoge/post.yml#/Hoge1"
                  - $ref: "../schemas/hoge/post.yml#/Hoge2"
                  - $ref: "../schemas/hoge/post.yml#/Hoge3"
                  - $ref: "../schemas/hoge/post.yml#/Hoge4"
                  - $ref: "../schemas/hoge/post.yml#/Hoge5"

みたいに複数の型が入り得るdataを定義したとき、一見

export type hogehogeGetResponseDataInner = Hoge1|Hoge2|Hoge3|Hoge4|Hoge5;

// ...

export interface hogehogeGetResponse {
    data: Array<hogehogeGetResponseDataInner>;
}

こんな感じでうまく生成できてる
だけどこいつを使おうとすると生成物の深いところでAPIから受け取ったjsonを整形しているコードがあり、ここでError起こる

export function hogehogeGetResponseDataInnerFromJSONTyped(json: any, ignoreDiscriminator: boolean): hogehogeGetResponseDataInner {
    if ((json === undefined) || (json === null)) {
        return json;
    }
    return { ...Hoge1FromJSONTyped(json, true), ...Hoge2FromJSONTyped(json, true), ...Hoge3FromJSONTyped(json, true), ...Hoge4FromJSONTyped(json, true), ...Hoge5FromJSONTyped(json, true)}

このHoge1FromJSONTyped()で、Hoge1にはないけどHoge2にはあるキーKがあったとしてレスポンスがHoge2型の時にHoge1FromJSONTyped()が実行されるとKがないって怒られる

このスクラップは2022/06/03にクローズされました