HTTP状态码——405 vs 501

Page content

HTTP状态码中有一些比较相似的状态码,容易让人产生混淆。其中比较冷门的一组是405(Method Not Allowed)与501(Not Implemented)。一个请求如果请求的方法不支持,不也算是没有实现么?这两者有何区别?我们就来仔细分析一下。

状态码的定义

说起状态码定义,最具权威性的就是rfc-2616 文档了:

6.5.5. 405 Method Not Allowed The 405 (Method Not Allowed) status code indicates that the method received in the request-line is known by the origin server but not supported by the target resource. The origin server MUST generate an Allow header field in a 405 response containing a list of the target resource’s currently supported methods.

405状态码表示请求中的方法(回忆一下,HTTP请求行由3部分组成,请求方法、请求资源以及请求版本号,例如:GET /some/amazing/resource HTTP/1.1)目标服务器认识,但是对于这次请求的资源,服务器无法用请求的方法响应。同时协议要求,响应头重必须包含Allow头,返回支持对应资源的请求方法。

6.6.2. 501 Not Implemented The 501 (Not Implemented) status code indicates that the server does not support the functionality required to fulfill the request. This is the appropriate response when the server does not recognize the request method and is not capable of supporting it for any resource.

501状态码则表示目的服务器不认识你请求的方法,对所有请求资源,也都无法响应了。

比较

仔细比较有细微差别。

405

405是说,这个请求方法(GET、PUT、DELETE)我们api server认识,但是对于你请求的资源(URI),你使用的方法我们不支持,这叫做方法不允许,同时response要包含一个Allow头,返回支持的HTTP请求方法。

举个例子,参考AWS S3对象生命周期(lifecycle)相关请求,这类资源只支持GET、PUT、DELETE方法,如果我们以HEAD方法请求lifecycle的资源,会返回以下错误:(注意Allow这个响应头)

curl -vvv -I "https://s3-ap-southeast-1.amazonaws.com/test-voidmain-method/?lifecycle"
* Trying 52.219.40.137...
* TCP_NODELAY set
* Connected to s3-ap-southeast-1.amazonaws.com (52.219.40.137) port 443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate: *.s3-ap-southeast-1.amazonaws.com
* Server certificate: DigiCert Baltimore CA-2 G2
* Server certificate: Baltimore CyberTrust Root
> HEAD /test-voidmain-method/?lifecycle HTTP/1.1
> Host: s3-ap-southeast-1.amazonaws.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 405 Method Not Allowed
HTTP/1.1 405 Method Not Allowed
< x-amz-request-id: 5ECA8F9A66AC236B
x-amz-request-id: 5ECA8F9A66AC236B
< x-amz-id-2: mM6O8oq7yytIjjg+CtB94SN09CZjZE975f1cIhLPzRlFmJEYclANCVxnzgyoAMdd3SBsm2uqBnQ=
x-amz-id-2: mM6O8oq7yytIjjg+CtB94SN09CZjZE975f1cIhLPzRlFmJEYclANCVxnzgyoAMdd3SBsm2uqBnQ=
< Allow: DELETE, GET, PUT
Allow: DELETE, GET, PUT
< Content-Type: application/xml
Content-Type: application/xml
< Transfer-Encoding: chunked
Transfer-Encoding: chunked
< Date: Sat, 30 Sep 2017 03:31:57 GMT
Date: Sat, 30 Sep 2017 03:31:57 GMT
< Server: AmazonS3
Server: AmazonS3
<
* Connection #0 to host s3-ap-southeast-1.amazonaws.com left intact

需要特别注意的是,响应头中的Allow头返回了GET、PUT和DELETE方法,也就是所有本资源支持的HTTP方法。

501

501是说,你的请求方法(例如PROPFIND方法),我们api server不认识(无法进行匹配),因此叫做未实现。 返回501主要是受限制于API Server实现。公司使用的jetty版本,只支持 rfc-2068 中定义的请求方法,因此使用PROPFIND方法请求时,就会返回501。例如:(考虑到保密等信息,隐藏掉了具体的域名和端口以及IP)

curl -vvv -X PROPFIND "http://example.com"
* Rebuilt URL to: http://example.com/
* Trying xx.xx.xx.xx...
* TCP_NODELAY set
* Connected to example.com (xx.xx.xx.xx) port 80 (#0)
> ASDFASDF / HTTP/1.1
> Host: example.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 501 Method PROPFIND is not defined in RFC 2068 and is not supported by the Servlet API
< Server: XXX
< Date: Sat, 30 Sep 2017 03:12:55 GMT
< Content-Length: 0
< Connection: keep-alive
< X-Application-Context: application
<
* Connection #0 to host example.com left intact

这个行为是jetty完成的:

10:51:05.498 415559652 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.jetty.server.Server REQUEST PROPFIND / on HttpChannelOverHttp@55741056{r=1,c=false,a=DISPATCHED,uri=/}
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.h.ContextHandler scope null||/ @ o.s.b.c.e.j.JettyEmbeddedWebAppContext@4d56b779{/,null,AVAILABLE}
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.h.ContextHandler context=||/ @ o.s.b.c.e.j.JettyEmbeddedWebAppContext@4d56b779{/,null,AVAILABLE}
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.jetty.server.session sessionManager=org.eclipse.jetty.server.session.HashSessionManager@6011c7cf
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.jetty.server.session session=null
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler servlet ||/ -> dispatcherServlet@7ef5559e==org.springframework.web.servlet.DispatcherServlet,-1,true
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler chain=apiFilter->applicationContextIdFilter->webRequestLoggingFilter->dispatcherServlet@7ef5559e==org.springframework.web.servlet.DispatcherServlet,-1,true
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call filter apiFilter
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call filter applicationContextIdFilter
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call filter webRequestLoggingFilter
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call servlet dispatcherServlet@7ef5559e==org.springframework.web.servlet.DispatcherServlet,-1,true
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.HttpConnection org.eclipse.jetty.server.HttpConnection$SendCallback@4521a8a2[PROCESSING][i=ResponseInfo{HTTP/1.0 501 Method PROPFIND is not defined in RFC 2068 and is not supported by the Servlet API ,-1,false},cb=org.eclipse.jetty.server.HttpChannel$CommitCallback@24c64723] generate: NEED_HEADER (null,[p=0,l=0,c=0,r=0],true)@START
10:51:05.499 415559653 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.HttpConnection org.eclipse.jetty.server.HttpConnection$SendCallback@4521a8a2[PROCESSING][i=ResponseInfo{HTTP/1.0 501 Method PROPFIND is not defined in RFC 2068 and is not supported by the Servlet API ,-1,false},cb=org.eclipse.jetty.server.HttpChannel$CommitCallback@24c64723] generate: FLUSH ([p=0,l=227,c=8192,r=227],[p=0,l=0,c=0,r=0],true)@COMPLETING

但是AWS并不是这么实现的:(AWS返回400)

curl -vvv -X ROPFAS "https://s3-ap-southeast-1.amazonaws.com/test-voidmain-method"
* Trying 52.219.32.101...
* TCP_NODELAY set
* Connected to s3-ap-southeast-1.amazonaws.com (52.219.32.101) port 443 (#0)
* TLS 1.2 connection using TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
* Server certificate: *.s3-ap-southeast-1.amazonaws.com
* Server certificate: DigiCert Baltimore CA-2 G2
* Server certificate: Baltimore CyberTrust Root
> ROPFAS /test-voidmain-method HTTP/1.1
> Host: s3-ap-southeast-1.amazonaws.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Date: Sat, 30 Sep 17 03:20:19 GMT
< Connection: close
< Transfer-Encoding: chunked
< x-amz-id-2: KMMj8crIsT1SPVKbK5sVnuxbqkNSMbzq4XHVAfIA9V6BUuzYIR7AX5XXeInRgJg9tlnaE9wO2ba0SvIIB/VSebPu87jR9Q5o
< x-amz-request-id: 69FAB6DEE37AC9B9
< Content-Type: application/xml
<
* Closing connection 0
<?xml version="1.0" encoding="UTF-8"?><Error><Code>BadRequest</Code><Message>An error occurred when parsing the HTTP request.</Message><RequestId>69FAB6DEE37AC9B9</RequestId><HostId>KMMj8crIsT1SPVKbK5sVnuxbqkNSMbzq4XHVAfIA9V6BUuzYIR7AX5XXeInRgJg9tlnaE9wO2ba0SvIIB/VSebPu87jR9Q5o</HostId></Error>

另外值得讨论的一点是,如果我们用一个Jetty认识(在 rfc-2068 中定义),但API不支持(没有注册requestMapper)的请求方法访问API,会返回什么:

curl -vvv -X PATCH "http://example.com/12345/?lifecycle"
* Trying xx.xx.xx.xx...
* TCP_NODELAY set
* Connected to example.com (xx.xx.xx.xx) port 80 (#0)
> PATCH /12345/?lifecycle HTTP/1.1
> Host: example.com
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 405 Request method 'PATCH' not supported
< Server: XXX
< Date: Sat, 30 Sep 2017 03:26:03 GMT
< Content-Length: 0
< Connection: keep-alive
< X-Application-Context: application
< Allow: DELETE, GET, PUT, HEAD, OPTIONS, POST
<
* Connection #0 to host example.com left intact

但这个返回并不标准,因为我请求的资源是lifecycle,lifecycle不支持POST、OPTIONS等请求。原因是,这个405也是Jetty返回的,Jetty无法理解业务资源类型,所以把所有我们支持的请求类型都返回了。

查看日志可以确认这个行为是Jetty做的:

10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.jetty.server.Server REQUEST PATCH / on HttpChannelOverHttp@462350f6{r=1,c=false,a=DISPATCHED,uri=/}
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.h.ContextHandler scope null||/ @ o.s.b.c.e.j.JettyEmbeddedWebAppContext@4d56b779{/,null,AVAILABLE}
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.h.ContextHandler context=||/ @ o.s.b.c.e.j.JettyEmbeddedWebAppContext@4d56b779{/,null,AVAILABLE}
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.jetty.server.session sessionManager=org.eclipse.jetty.server.session.HashSessionManager@6011c7cf
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.jetty.server.session session=null
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler servlet ||/ -> dispatcherServlet@7ef5559e==org.springframework.web.servlet.DispatcherServlet,-1,true
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler chain=apiFilter->applicationContextIdFilter->webRequestLoggingFilter->dispatcherServlet@7ef5559e==org.springframework.web.servlet.DispatcherServlet,-1,true
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call filter apiFilter
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call filter applicationContextIdFilter
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call filter webRequestLoggingFilter
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.e.j.s.ServletHandler call servlet dispatcherServlet@7ef5559e==org.springframework.web.servlet.DispatcherServlet,-1,true
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.s.w.s.DispatcherServlet DispatcherServlet with name 'dispatcherServlet' processing PATCH request for [/]
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.s.b.a.e.m.EndpointHandlerMapping Looking up handler method for path /
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.s.b.a.e.m.EndpointHandlerMapping Did not find handler method for [/]
10:49:03.228 415437382 [6061] [qtp2047899112-1676226 - /] DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping Looking up handler method for path /ExceptionHandlerExceptionResolver Resolving exception from handler [null]: org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'PATCH' not supported
10:49:03.229 415437383 [6061] [qtp2047899112-1676226 - /] DEBUG o.s.w.s.m.a.ResponseStatusExceptionResolver Resolving exception from handler [null]: org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'PATCH' not supported

结合以上实验,我们可以看出jetty处理区分405和501的方法是:在 rfc-2068中定义过的方法,如果请求的url无法匹配,就返回405,同时返回所有API中定义过的请求方法;如果请求的方法没有在 rfc-2068 中定义,就直接返回501。

如何使用405与501?

那么对于应用开发者来说,如何使用这两者呢?

分析下来,最合理的处理方式是,如果请求方法与请求资源能够匹配,且能正常处理,则走正常逻辑;如果没有匹配成功,则先解析请求需要的资源,根据资源搜索支持此资源的请求方法,并将这些方法拼接到Allow头返回给客户端;如果根据资源无法找到任何能处理的方法,则返回501。

另外需要注意的是,由于HTTP状态码5xx一般认为是服务端错误,所以可能有公司的监控系统监控了所有5xx请求,所以501可能会影响监控报警,需要注意。如果实现合理,可以在监控中忽略501状态码。