场景描述
比如我们需要对 API 限流抛出的异常进行接管,并重写响应消息,首先应用中间件:
1use Dingo\Api\Routing\Router; 2 3$api->group([ 4 'middleware' => 'api.throttle', // 限流中间件 5 'expires' => 1, // 时间范围,单位“分” 6 'limit' => 2, // 时间范围内请求次数 7], function (Router $api) { 8 9 $api->post('auth/login', 'LoginController@login'); 10});
使用 Postman 进行接口调试,我们会发现在正常请求阶段会多出三个响应头:
1X-RateLimit-Limit # 时间范围内可请求次数 2X-RateLimit-Remaining # 时间范围内剩余可请求次数 3X-RateLimit-Reset # 到期时间戳
继续重复请求两次后会得到类似如下结果(修改过):
1{ 2 "message": "You have exceeded your rate limit.", 3 "result": 0, 4 "status_code": 429 5}
异常接管
这里有两种接管方式
-
单一异常接管:
建议在
App\Providers\AppServiceProvider文件中的register()方法内进行编写:1$this->app->make(Dingo\Api\Exception\Handler::class)->register(function (RateLimitExceededException $e) { 2 return response([ 3 'message' => '当前请求太过频繁', 4 'result' => 0, 5 'status_code' => 429 6 ])->setStatusCode($e->getStatusCode())->withHeaders($e->getHeaders()); 7});如果无需使用状态码,可去掉 setStatusCode 方法,仅保留 withHeaders 即可。去掉后 HTTP 状态码响应为 200。 -
多异常接管:
顾名思义,单一异常接管仅适用于单一的服务场景,而 Dingo API 提供了多项服务,如果应用多项时,上述方式就不适用了。
首先,我们在
app/Exceptions目录内创建名为DingoExceptionHandler的类文件,同样我们以限流异常示例,内容如下:1<?php 2 3namespace App\Exceptions; 4 5use Dingo\Api\Contract\Debug\ExceptionHandler; 6use Dingo\Api\Exception\Handler as DingoHandler; 7use Dingo\Api\Exception\RateLimitExceededException; 8use Exception; 9 10class DingoExceptionHandler extends DingoHandler implements ExceptionHandler { 11 12 public function handle(Exception $exception) { 13 if ($exception instanceof RateLimitExceededException) { 14 return response([ 15 'message' => '当前请求太过频繁', 16 'result' => 0, 17 'status_code' => 429 18 ])->withHeaders($exception->getHeaders()); 19 } 20 21 // TODO: 此处可对其它异常进行同样方式的处理 22 23 return parent::handle($exception); 24 } 25}上方类文件还未应用,此时应当将其注入到框架容器中。
打开
App\Providers\AppServiceProvider文件,在register()方法中添加:1$this->app->singleton('api.exception', function () { 2 return new App\Exceptions\DingoExceptionHandler( 3 $this->app['Illuminate\Contracts\Debug\ExceptionHandler'], 4 config('api.errorFormat'), 5 config('api.debug') 6 ); 7});至此接管完成,再次进行请求测试,响应结果变更为:
1{ 2 "message": "当前请求太过频繁", 3 "result": 0, 4 "status_code": 429 5}响应头中会多出一项
Retry-After,值为剩余可请求时间,单位秒,即 n 秒后允许请求。
说句题外话,之前看到网上很多人在 Lumen 框架中对服务的注册都是通过 $app->register() 写在 bootstrap/app.php 文件内,
个人建议不要这么做,应当统一写在 app\Providers\AppServiceProvider.php 文件内,因为在 app.php 中人家已经注册了这玩意儿
1$app->register(App\Providers\AppServiceProvider::class);
那何不规范的写在 Providers 里面呢?例如:
1<?php 2 3namespace App\Providers; 4 5use App\Exceptions\DingoExceptionHandler; 6use App\Http\DingoAPI\StrictHeaderAccept; 7use Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider; 8use Dingo\Api\Http\Validation\Accept; 9use Dingo\Api\Provider\LumenServiceProvider as DingoAPI; 10use Illuminate\Redis\RedisServiceProvider; 11use Illuminate\Support\ServiceProvider; 12 13class AppServiceProvider extends ServiceProvider { 14 15 /** 16 * Register any application services. 17 * 18 * @return void 19 */ 20 public function register() { 21 // Dingo API 22 $this->app->register(DingoAPI::class); 23 24 // Overwrite request header Accept verify 25 $this->app->singleton(Accept::class, function () { // Dingo API Accept 严格头的简易白名单方式,StrictHeaderAccept 类参考下方 26 return new Accept(new StrictHeaderAccept( 27 config('api.standardsTree'), 28 config('api.subtype'), 29 config('api.version'), 30 config('api.defaultFormat')), 31 config('api.strict')); 32 }); 33 34 // Overwrite rate limit exception render 35 $this->app->singleton('api.exception', function () { 36 return new DingoExceptionHandler( 37 $this->app['Illuminate\Contracts\Debug\ExceptionHandler'], 38 config('api.errorFormat'), 39 config('api.debug') 40 ); 41 }); 42 43 // Redis 44 $this->app->register(RedisServiceProvider::class); 45 46 // IDE Helper 47 if ($this->app->environment() !== 'production') { 48 $this->app->register(IdeHelperServiceProvider::class); 49 } 50 } 51}
StrictHeaderAccept.php 内容:
1<?php 2 3namespace App\Http\DingoAPI; 4 5use Dingo\Api\Http\Parser\Accept; 6use Illuminate\Http\Request; 7 8class StrictHeaderAccept extends Accept { 9 10 public function parse(Request $request, $strict = false) { 11 if (in_array($request->getPathInfo(), config('whitelist.request.header'))) { 12 $strict = false; 13 } 14 15 return parent::parse($request, $strict); 16 } 17}
