Lumen 中對 Dingo API 異常接管並自定義響應結果

場景描述

比如我們需要對 API 限流拋出的異常進行接管,並重寫響應消息,首先應用中間件:

use Dingo\Api\Routing\Router;

$api->group([
    'middleware' => 'api.throttle', // 限流中間件
    'expires' => 1, // 時間範圍,單位“分”
    'limit' => 2, // 時間範圍內請求次數
], function (Router $api) {

    $api->post('auth/login', 'LoginController@login');
});

使用 Postman 進行接口調試,我們會發現在正常請求階段會多出三個響應頭:

X-RateLimit-Limit      # 時間範圍內可請求次數
X-RateLimit-Remaining  # 時間範圍內剩餘可請求次數
X-RateLimit-Reset      # 到期時間戳

繼續重複請求兩次後會得到類似如下結果(修改過):

{
    "message": "You have exceeded your rate limit.",
    "result": 0,
    "status_code": 429
}

異常接管

這裏有兩種接管方式

  1. 單一異常接管:

    建議在 App\Providers\AppServiceProvider 文件中的 register() 方法內進行編寫:

    $this->app->make(Dingo\Api\Exception\Handler::class)->register(function (RateLimitExceededException $e) {
       return response([
            'message' => '當前請求太過頻繁',
            'result' => 0,
            'status_code' => 429
        ])->setStatusCode($e->getStatusCode())->withHeaders($e->getHeaders());
    });
    
     如果無需使用狀態碼,可去掉 setStatusCode 方法,僅保留 withHeaders 即可。去掉後 HTTP 狀態碼響應爲 200。
    
  2. 多異常接管:

    顧名思義,單一異常接管僅適用於單一的服務場景,而 Dingo API 提供了多項服務,如果應用多項時,上述方式就不適用了。

    首先,我們在 app/Exceptions 目錄內創建名爲 DingoExceptionHandler 的類文件,同樣我們以限流異常示例,內容如下:

    <?php
    
    namespace App\Exceptions;
    
    use Dingo\Api\Contract\Debug\ExceptionHandler;
    use Dingo\Api\Exception\Handler as DingoHandler;
    use Dingo\Api\Exception\RateLimitExceededException;
    use Exception;
    
    class DingoExceptionHandler extends DingoHandler implements ExceptionHandler {
    
        public function handle(Exception $exception) {
            if ($exception instanceof RateLimitExceededException) {
                return response([
                    'message' => '當前請求太過頻繁',
                    'result' => 0,
                    'status_code' => 429
                ])->withHeaders($exception->getHeaders());
            }
    
            // TODO: 此處可對其它異常進行同樣方式的處理
    
            return parent::handle($exception);
        }
    }
    

    上方類文件還未應用,此時應當將其注入到框架容器中。

    打開 App\Providers\AppServiceProvider 文件,在 register() 方法中添加:

    $this->app->singleton('api.exception', function () {
        return new App\Exceptions\DingoExceptionHandler(
            $this->app['Illuminate\Contracts\Debug\ExceptionHandler'],
            config('api.errorFormat'),
            config('api.debug')
        );
    });
    

    至此接管完成,再次進行請求測試,響應結果變更爲:

    {
        "message": "當前請求太過頻繁",
        "result": 0,
        "status_code": 429
    }
    

    響應頭中會多出一項 Retry-After,值爲剩餘可請求時間,單位秒,即 n 秒後允許請求。


說句題外話,之前看到網上很多人在 Lumen 框架中對服務的註冊都是通過 $app->register() 寫在 bootstrap/app.php 文件內,

個人建議不要這麼做,應當統一寫在 app\Providers\AppServiceProvider.php 文件內,因爲在 app.php 中人家已經註冊了這玩意兒

$app->register(App\Providers\AppServiceProvider::class);

那何不規範的寫在 Providers 裏面呢?例如:

<?php

namespace App\Providers;

use App\Exceptions\DingoExceptionHandler;
use App\Http\DingoAPI\StrictHeaderAccept;
use Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider;
use Dingo\Api\Http\Validation\Accept;
use Dingo\Api\Provider\LumenServiceProvider as DingoAPI;
use Illuminate\Redis\RedisServiceProvider;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider {

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register() {
        // Dingo API
        $this->app->register(DingoAPI::class);

        // Overwrite request header Accept verify
        $this->app->singleton(Accept::class, function () { // Dingo API Accept 嚴格頭的簡易白名單方式,StrictHeaderAccept 類參考下方
            return new Accept(new StrictHeaderAccept(
                config('api.standardsTree'),
                config('api.subtype'),
                config('api.version'),
                config('api.defaultFormat')),
                config('api.strict'));
        });

        // Overwrite rate limit exception render
        $this->app->singleton('api.exception', function () {
            return new DingoExceptionHandler(
                $this->app['Illuminate\Contracts\Debug\ExceptionHandler'],
                config('api.errorFormat'),
                config('api.debug')
            );
        });

        // Redis
        $this->app->register(RedisServiceProvider::class);

        // IDE Helper
        if ($this->app->environment() !== 'production') {
            $this->app->register(IdeHelperServiceProvider::class);
        }
    }
}

StrictHeaderAccept.php 內容:

<?php

namespace App\Http\DingoAPI;

use Dingo\Api\Http\Parser\Accept;
use Illuminate\Http\Request;

class StrictHeaderAccept extends Accept {

    public function parse(Request $request, $strict = false) {
        if (in_array($request->getPathInfo(), config('whitelist.request.header'))) {
            $strict = false;
        }
        return parent::parse($request, $strict);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章