OpenAPI Generatorでエラーレスポンスって拾えないのかな?かな?
OpenAPI GeneratorでTS用の型生成とfetchをラップしたクラスを生成してる
- yamlをゴリゴリ書く
- openapi-generatorコマンド叩く
- 型とfetchのラップクラス生成される
- APIとのやりとりおよびそのResponse/Requestの型は生成物を使用
こうしてたんだけど
oneOfが使えなかったり、queryパラメータでオブジェクトを指定してもキーバリューがうまく扱えてなかったり、その他にもうんざりするような未対応事項が色々あって、ちょっとパッチ当てるくらいなら頑張ってみようかなと思ったけど他のツールとも依存関係にありすぎてお気持ちポッキリ折られた
そんで今日改めて認知してたけど自分の中でStay状態にしてたエラーレスポンスを拾えてない件を指摘され、「使わないようにする」という手も視野に入れつつ、エラーレスポンスを拾えないか模索してみんとてするなり
思い出してきた
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;
}
};
で、👆 上のコード内、 👇 下記部分でステータスコードが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;
}
呼び出し元ではBaseAPIを継承して、その中で各エンヨポインヨごとの関数作ってる
その関数内で 👆 のrequest()
呼んでて、そいつを引数として今度はBaseAPI
のJSONApiResponse()
に投げてる
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));
あー、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));
こいつもただのラッパーだった
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以外のレスポンスの第一階層のキーでも判別したい
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
という型になってるけど、こいつがどこからも呼び出されてない
しっかりドキュメント読む
あっ……
[Spring] can't handle multiple responses #1096
I wonder that this issue has such a low activity. Is there any recommended workaround, or do you plan to fix it in an upcoming release?
うんうん
あれ、てか普通にエラーオブジェクト潰してなかったから呼び出しもとでcatchできるのか
例えば
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
がないって怒られる
最近こんなの見つけたからOpenAPIGeneratorから移りたみisある