修改 Request headers 不完全攻略
如果某个接口即支持 form 请求又支持 api 请求,或者对于一些公共的处理程序(比如认证失败,输入校验失败等),我们往往需要根据请求类型来返回对应的 response 。
例如 Laravel 5.3 的 ValidatesRequests 中:
protected function buildFailedValidationResponse(Request $request, array $errors)
{
if ($request->expectsJson()) {
return new JsonResponse($errors, 422);
}
return redirect()->to($this->getRedirectUrl())
->withInput($request->input())
->withErrors($errors, $this->errorBag());
}
Request::expectsJson
实现如下:
public function expectsJson()
{
return ($this->ajax() && ! $this->pjax()) || $this->wantsJson();
}
而 Request::wantsJson
是通过 request headers 中的 Accept
来判断是否需要返回 JSON 响应。一般情况下,Api 客户端会在发送 HTTP 请求时设置头部 Accept: application/json
。
那么问题来了,如果 api client 没有传 Accept
头部信息或者里面没有包含 json
字样, $request->wantsJson()
就会返回 false
,从而导致给 api client 返回了不正确的响应。
解决方案一
可以在 expectsJson
后面再跟一个判断,比如:
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson() || $request->is('api/*')) {
return response()->json(['error' => 'Unauthenticated.'], 401);
}
return redirect()->guest('login');
}
Request::expectsJson
是 Laravel 5.3 新增的方法,使用 5.3 之前版本的话,可能需要这样写:
if (
($request->ajax() && ! $request->pjax()) ||
$request->wantsJson() ||
$request->is('api/*')
) {
//
}
或者把这一坨放到一个 helper 函数里方便调用:
function is_api_request()
{
$request = app('request');
return $request->...
}
需要注意的是 Laravel Framework 内部也使用了这个判断,例如本文开头提到的校验失败响应,所以要把相关的地方都重写下。并且每次升级 Framework 版本时还要检查一遍是不是内部又有别的地方使用了此类判断,或者之前重写的方法有变动。
显然,这个方案相当笨拙、不符合 Laravel 的优雅哲学。
解决方案二
既然服务端要兼容还挺麻烦,那么就强制所有的 api client 添加 Accept: application/json
请求头,违者后果自负。
这下整片雾霾都散了...
等等,老大刚说这个问题还是得服务端解决下。因为 iOS 客户端现在有几处 bug 就是这个问题导致的,而上个 iOS 版本被苹果审核了 3 个月,更何况 PHP 是世界上最好的语言,所以...
解决方案三
既然 Request::wantsJson
是通过头部信息判断的,那么我们能不能直接修改头部的 Accept
字段值添加 json
类型?答案是必须能。
- Where
借助 Laravel 强大的 IoC 技术, 我们可以注册 request 的绑定回调,比如app()->rebinding('request', ...)
. - How
如果$request->header('Accept')
不包含json
类型,我们就把application/json
添加上去,并通过$request->headers->set()
修改 header 值。 - When
越早越好。推荐放在某个非 deferred 的 service provider 的register
方法里。 - :warning: Note
判断原始值是否包含 json 类型时不能用expectsJson
,wantsJson
,accepts
,acceptsJson
等方法。因为Request
对Accept
值是懒加载并缓存的,所以调用过wantsJson
后再通过$request->headers->set('Accept', ...)
修改的值会不起作用。 - And
顺便把$_SERVER
相关值也修改下,万一有人用了呢。 -
Show me the code
class AppServiceProvider extends ServiceProvider { /** * Register any application services. */ public function register() { $this->addAcceptableJsonType(); } /** * Add "application/json" to the "Accept" header for the current request. */ protected function addAcceptableJsonType() { $this->app->rebinding('request', function ($app, $request) { if ($request->is('api/*')) { $accept = $request->header('Accept'); if (! str_contains($accept, ['/json', '+json'])) { $accept = rtrim('application/json,'.$accept, ','); $request->headers->set('Accept', $accept); $request->server->set('HTTP_ACCEPT', $accept); $_SERVER['HTTP_ACCEPT'] = $accept; } } }); } }
解决方案四
感谢 @Ryan 和 @oustn 的提醒,也可以把上述代码放到一个 middleware 里,然后给相关的请求添加这个 middleware.
至于用 middleware 还是用 service provider ,对于解决这个问题没多大区别,看个人喜好。详细讨论参见下面的评论。
推荐文章: