Express + RoutingControllers で APIの入力エラーを400 Bad RequestでJSON形式で詳細を返したい


# やりたかったこと

今までこんな形でAPIの結果をすべて`200`で返していました

```ts
interface SuccessResult {
  success: true;
}

interface ErrorResult {
  success:false;
}

interface ApiXSuccessResult extends SucccessResult {
  data: {
    ...
  }
}
```

ですが正常系のみ`200`で返し、エラーがあった場合は、ステータス`400`を使い、具体的なエラーメッセージはResponse bodyに`json`形式で下記のように返すように変更しようとしました。

```json
Status: 400 Bad Request
Content-Type: application/json
{ code: "USER_INPUT_ERROR", reason: "Length of nickname must be under 30"}

```

# 最初にやったこと

https://github.com/typestack/routing-controllers#throw-http-errors

を参考に以下のような`Error`クラスを実装して例外を`throw`させました。

```ts

import {HttpError} from "routing-controllers";

export class InputError extends HttpError {
    constructor() {
        super(404, "User not found!");
    }

    toJSON() {
        return {
            code: "USER_INPUT_ERROR",
            reason: "Length of nickname must be under 30"
        }
    }
}
```

# トラブル発生

ところが、`toJSON`の内容はResponseのBodyに反映されないどころか、そもそも`toJSON`を一切通過しません。

`toJSON`は`BaseDrive.ts`の`processJsonError`で呼び出されています。
ところがこちらの`processJsonError`すら呼び出されていません。

https://github.com/typestack/routing-controllers/blob/4a56d176db77bc081dfcd3d8550e8433b5bc476e/src/driver/BaseDriver.ts

```ts:routing-controllers/src/driver/BaseDriver.ts
    protected processJsonError(error: any) {
        if (!this.isDefaultErrorHandlingEnabled)
            return error;

        if (typeof error.toJSON === "function")
            return error.toJSON();
        
        let processedError: any = {};
        if (error instanceof Error) {
            const name = error.name && error.name !== "Error" ? error.name : error.constructor.name;
            processedError.name = name;

            if (error.message)
                processedError.message = error.message;
            if (error.stack && this.developmentMode)
                processedError.stack = error.stack;

            Object.keys(error)
                .filter(key => key !== "stack" && key !== "name" && key !== "message" && (!(error instanceof HttpError) || key !== "httpCode"))
                .forEach(key => processedError[key] = (error as any)[key]);

            if (this.errorOverridingMap)
                Object.keys(this.errorOverridingMap)
                    .filter(key => name === key)
                    .forEach(key => processedError = this.merge(processedError, this.errorOverridingMap[key]));

            return Object.keys(processedError).length > 0 ? processedError : undefined;
        }

        return error;
    }
```

さらに`processJsonError`を呼び出す ExpressDriveのhandleErrorを確認します。
こちらの`handleError`は呼び出されていました。しかし`isDefaultErrorHandlingEnabled`が`false`なために一切の処理が無視されていました。

https://github.com/typestack/routing-controllers/blob/53264285466a0472ef9644f28f0b0a1dca0df939/src/driver/express/ExpressDriver.ts

```ts:routing-controllers/src/driver/express/ExpressDriver.ts
    /**
     * Handles result of failed executed controller action.
     */
    handleError(error: any, action: ActionMetadata | undefined, options: Action): any {
        if (this.isDefaultErrorHandlingEnabled) {
            const response: any = options.response;

            // set http code
            // note that we can't use error instanceof HttpError properly anymore because of new typescript emit process
            if (error.httpCode) {
                response.status(error.httpCode);
            } else {
                response.status(500);
            }

            // apply http headers
            if (action) {
                Object.keys(action.headers).forEach(name => {
                    response.header(name, action.headers[name]);
                });
            }

            // send error content
            if (action && action.isJsonTyped) {
                response.json(this.processJsonError(error));
            } else {
                response.send(this.processTextError(error)); // todo: no need to do it because express by default does it
            }
        }
        options.next(error);
    }
```

次に、`isDefaultErrorHandlingEnabled`がどこで設定されているか確認します。
`routing-controllers/src/index.ts`の`createExecutor`関数で初期化オプションの`RoutingControllersOptions`の`defaultErrorHandler`設定値を引き継いでいました。

https://github.com/typestack/routing-controllers/blob/4a56d176db77bc081dfcd3d8550e8433b5bc476e/src/index.ts

```ts:routing-controllers/src/index.ts
/**
 * Registers all loaded actions in your express application.
 */
export function createExecutor<T extends BaseDriver>(driver: T, options: RoutingControllersOptions = {}): void {
:
    if (options.defaultErrorHandler !== undefined) {
        driver.isDefaultErrorHandlingEnabled = options.defaultErrorHandler;
    } else {
        driver.isDefaultErrorHandlingEnabled = true;
    }
```

ということで初期化オプションの値を変更しました。

```diff
    {
-      defaultErrorHandler: false,
+      defaultErrorHandler: true,
      controllers: [`${__dirname}/controllers/**/*.ts`],
      middlewares: [`${__dirname}/middlewares/*.ts`]
    };
```

# まだまだ問題発生

ところがそれでも `toJSON`は呼び出されません。先ほどのコードの下記の部分で、`error.toJSON`が`undefined`になっているのです。

```ts:routing-controllers/src/driver/BaseDriver.ts
    protected processJsonError(error: any) {
:

        if (typeof error.toJSON === "function")
            return error.toJSON();
```

[サンプル](https://github.com/typestack/routing-controllers#throw-http-errors)通り以下をコンストラクターに追加してあげる必要があります。

```diff:api-error

class ApiError extends HttpError {

    constructor(...) {
      :
+        Object.setPrototypeOf(this, ApiError.prototype);
    }

```

これで `toJSON`が呼び出されるようになりましたが・・・

# まだまだ問題が発生

Nuxt側からaxiosを使って呼び出すと `Error occured while trying to proxy to: `エラーがでます。

`http-proxy-middleware`の`defaultErrorHandler`でこのエラーが吐き出されていました。

```js
function defaultErrorHandler(err, req, res) {
  var host = req.headers && req.headers.host
  var code = err.code

  if (res.writeHead && !res.headersSent) {
    if (/HPE_INVALID/.test(code)) {
      res.writeHead(502)
    } else {
      switch (code) {
        case 'ECONNRESET':
        case 'ENOTFOUND':
        case 'ECONNREFUSED':
          res.writeHead(504)
          break
        default:
          res.writeHead(500)
      }
    }
  }

  res.end('Error occured while trying to proxy to: ' + host + req.url)
}
```

ということでデフォルトハンドラーとして空のハンドラーを設定してあげました。

```ts

import { Middleware, ExpressErrorMiddlewareInterface } from "routing-controllers";

@Middleware({ type: "after" })
export class DefaultErrorHandler implements ExpressErrorMiddlewareInterface {
  error(error, request, response, next) {
  }
}
```

これで想定通りエラーのHTTPステータスを吐き出しつつReponse BodyにエラーメッセージをJSON形式で書き出す処理が動きました。