带有请求正文的HTTP GET

我正在为我们的应用程序开发一个新的RESTful Web服务。

在对某些实体执行GET时,客户端可以请求实体的内容。如果他们想添加一些参数(例如排序列表),他们可以在查询字符串中添加这些参数。

或者,我希望人们能够在请求正文中指定这些参数。HTTP/1.1似乎没有明确禁止这样做。这将允许他们指定更多信息,可能会更容易指定复杂的XML请求。

我的问题:

  • 这完全是个好主意吗?
  • HTTP客户端在GET请求中使用请求主体会有问题吗?

https://www.rfc-editor.org/rfc/rfc2616

1966963 次浏览

虽然你可以这样做,因为HTTP规范没有明确排除它,我建议避免它,仅仅因为人们不希望事情以这种方式工作。HTTP请求链中有许多阶段,虽然它们“大部分”符合HTTP规范,但你唯一可以保证的是,它们的行为将与Web浏览器传统上使用的一样(我在考虑透明代理、加速器、A/V工具包等)。

这就是稳健性原则背后的精神大致“在你接受的东西上保持自由,在你发送的东西上保持保守”,你不想在没有充分理由的情况下突破规范的界限。

但是,如果你有一个很好的理由,那就去做吧。

我不建议这样做,它违背了标准的做法,并且没有提供那么多的回报。您希望保留内容的主体,而不是选项。

如果您尝试利用缓存,您可能会遇到问题。代理不会在GET主体中查看参数是否对响应有影响。

您尝试实现的目标已经使用更常见的方法完成了很长时间,并且不依赖于使用GET的有效负载。

您可以简单地构建特定的搜索媒体类型,或者如果您想更具RESTful,请使用OpenSearch之类的东西,并将请求POST到服务器指示的URI,例如 /search.然后服务器可以生成搜索结果或构建最终URI并使用303重定向。

这具有遵循传统PRG方法、帮助缓存中介缓存结果等优点。

也就是说,URI无论如何都会为非ASCII的任何东西编码,应用程序/x-www-form-urlencoded和multipart/form-data也是如此。如果您的意图是支持ReSTful方案,我建议使用它而不是创建另一种自定义json格式。

Roy Fielding关于在GET请求中包含身体的评论

是的。换句话说,任何HTTP请求消息都允许包含消息主体,因此必须考虑到这一点来解析消息。然而,GET的服务器语义学受到限制,以至于主体(如果有)对请求没有语义意义。解析要求与方法语义学要求是分开的。

所以,是的,你可以用GET发送一个主体,不,这样做永远没有用。

这是HTTP/1.1分层设计的一部分,一旦规范被分区(半成品),它将再次变得清晰。

……罗伊

是的,你可以使用GET发送请求正文,但它不应该有任何意义。如果你通过在服务器和根据内容更改您的回复上解析它来赋予它意义,那么你将忽略HTTP/1.1规范,第4.3节中的此建议:

…如果请求方法不包括实体主体的定义语义学,则在处理请求时忽略消息主体应该

以及HTTP/1.1规范,第9.3节中GET方法的描述:

GET方法意味着检索由Request est-URI标识的任何信息([…])。

它指出请求主体不是GET请求中资源标识的一部分,只有请求URI。

更新

作为“HTTP/1.1规范”引用的RFC2616现已过时。2014年,它被RFC 7230-7237所取代。“处理请求时应该忽略消息主体”的报价已被删除。现在只是“请求消息帧独立于方法语义学,即使该方法没有定义消息主体的任何用途”

HTTP 1.1 2014规范

GET请求消息中的有效负载没有定义的语义学;在GET请求上发送有效负载正文可能会导致某些现有实现拒绝该请求。

那么不一致的bas64编码标头呢?“某些东西GAPP-PARAMS:sdfSD45fdg45/aS”

长度限制嗯。你不能让你的POST处理区分含义吗?如果你想要简单的参数,比如排序,我不明白为什么这会是一个问题。我想这是你担心的确定性。

你可以发送一个带有身体的GET或发送一个POST并放弃RESTish宗教信仰(这并不坏,5年前只有一个信仰成员-他的评论链接在上面)。

两者都不是很好的决策,但是发送GET主体可以防止某些客户端和某些服务器出现问题。

执行POST可能会遇到某些RESTish框架的障碍。

Julian Reschke建议使用像“SEARCH”这样的非标准HTTP标头,这可能是一个优雅的解决方案,只是它更不可能得到支持。

列出能够和不能执行上述每一项操作的客户端可能是最有效的。

无法发送带有body的GET的客户端(据我所知):

  • XmlHTTP请求小提琴

可以发送带有主体的GET的客户端:

  • 大多数浏览器

可以从GET检索主体的服务器和库:

  • apache
  • php

从GET中剥离主体的服务器(和代理):

  • ?

如果您真的想将可缓存的JSON/XML主体发送到Web应用程序,那么放置数据的唯一合理位置是使用RFC4648:使用URL和文件名安全字母进行Base 64编码编码的查询字符串。当然,您可以对JSON进行urlencode并将is放入URL参数的值中,但Base64给出的结果较小。请记住,有URL大小限制,请参阅不同浏览器中URL的最大长度是多少?

你可能认为Base64的填充=字符可能对URL的参数值不利,但似乎不是-请参阅此讨论:http://mail.python.org/pipermail/python-bugs-list/2007-February/037195.html。但是,你不应该在没有参数名称的情况下放置编码数据,因为带有填充的编码字符串将被解释为具有空值的参数键。我用的是?_b64=<encodeddata>.

恕我直言,您可以在URL中发送JSON编码(即encodeURIComponent),这样您就不会违反HTTP规范并将您的JSON发送到服务器。

恢复客户端REST控制台都不支持,但curl支持。

HTTP规范在第4.3节中说

如果请求方法的规范(第5.1.1节)不允许在请求中发送实体主体,则不得将消息主体包含在请求中。

第5.1.1节将我们重定向到9. x节的各种方法。它们都没有明确禁止包含消息正文。但是…

第5.2节表示

由Internet请求标识的确切资源是通过检查Request est-URI和Host标头字段来确定的。

第9.3节表示

GET方法意味着检索由Request est-URI标识的任何信息(以实体的形式)。

这一起表明,在处理GET请求时,服务器不是选填/必填来检查除请求URI和主机标头字段之外的任何东西。

总而言之,HTTP规范不会阻止您使用GET发送消息正文,但如果不是所有服务器都支持它,它就不会让我感到惊讶。

哪个服务器会忽略它?-fijiaaronAug 30'12 at 21:27

例如,谷歌比忽略它更糟糕,它会认为它是错误

自己用一个简单的netcat试试:

$ netcat www.google.com 80GET / HTTP/1.1Host: www.google.comContent-length: 6
1234

(1234内容后跟CR-LF,因此总共有6个字节)

你会得到:

HTTP/1.1 400 Bad RequestServer: GFE/2.0(....)Error 400 (Bad Request)400. That’s an error.Your client has issued a malformed or illegal request. That’s all we know.

您还会收到来自必应、Apple等的400 Bad Request,这些请求由AkamaiGhost提供服务。

因此,我不建议将GET请求与主体实体一起使用。

Elasticsearch接受带有主体的GET请求。甚至看起来这是首选方式:Elasticsearch指南

一些客户端库(如Ruby驱动程序)可以在开发模式下将哭泣命令记录到stdout,并且它正在广泛使用这种语法。

我对REST协议不支持OOP感到不安,Get方法就是证明。作为一种解决方案,您可以将DTO序列化为JSON,然后创建一个查询字符串。在服务器端,您可以将查询字符串反序列化为DTO。

来看看:

基于消息的方法可以帮助您解决获取方法的限制。您将能够像请求正文一样发送任何DTO

Nelibur Web服务框架提供了您可以使用的功能

var client = new JsonServiceClient(Settings.Default.ServiceAddress);var request = new GetClientRequest{Id = new Guid("2217239b0e-b35b-4d32-95c7-5db43e2bd573")};var response = client.Get<GetClientRequest, ClientResponse>(request);
as you can see, the GetClientRequest was encoded to the following query string
http://localhost/clients/GetWithResponse?type=GetClientRequest&data=%7B%22Id%22:%2217239b0e-b35b-4d32-95c7-5db43e2bd573%22%7D

RFC 2616,第4.3节,“消息正文”:

服务器应该在任何请求上读取并转发消息主体;如果请求方法不包括实体主体的定义语义学,那么在处理请求时应该忽略消息体。

也就是说,服务器应该总是从网络中读取任何提供的请求正文(检查Content-L长度或读取分块正文,等)。此外,代理应该转发他们收到的任何这样的请求正文。然后,如果RFC为给定的方法的主体定义了语义学,服务器实际上可以使用请求正文来生成响应。然而,如果RFC不要为主体定义了语义学,那么服务器应该忽略它。

这与上面菲尔丁的报价一致。

第9.3节,"GET",描述了GET方法的语义学,没有提到请求主体。因此,服务器应该忽略它在GET请求中收到的任何请求主体。

例如,它适用于Curl、Apache和PHP。

php文件:

<?phpecho $_SERVER['REQUEST_METHOD'] . PHP_EOL;echo file_get_contents('php://input') . PHP_EOL;

控制台命令:

$ curl -X GET -H "Content-Type: application/json" -d '{"the": "body"}' 'http://localhost/test/get.php'

输出:

GET{"the": "body"}

根据XMLHttpRequest,它是无效的。从标准

4.5.6send()方法

client . send([body = null])

Initiates the request. The optional argument provides the requestbody. The argument is ignored if request method is GET or HEAD.

Throws an InvalidStateError exception if either state is notopened or the send() flag is set.

The send(body) method must run these steps:

  1. If state is not opened, throw an InvalidStateError exception.
  2. If the send() flag is set, throw an InvalidStateError exception.
  3. If the request method is GET or HEAD, set body to null.
  4. If body is null, go to the next step.

Although, I don't think it should because GET request might need big body content.

So, if you rely on XMLHttpRequest of a browser, it's likely it won't work.

我向IETF HTTP WG提出了这个问题。Roy Fielding(1998年超文本传输协议/1.1文档的作者)的评论是

"…一个实现将被破坏,除了解析和丢弃接收到的主体之外,还可以做任何事情。

RFC 7213(HTTPbis)声明:

"GET请求消息中的有效负载没有定义的语义学;"

现在看来很明显,其目的是禁止GET请求主体上的语义含义,这意味着请求主体不能用于影响结果。

如果您在GET上包含一个主体,那么那里的代理将绝对以各种方式破坏您的请求。

总之,不要这样做。

您有一个选项列表,比使用GET的请求主体要好得多。

让我们假设您为每个类别都有类别和项目。两者都由一个id标识(为了这个例子的缘故,“catid”/“itemid”)。您想根据另一个参数“sortby”以特定的“顺序”进行排序。您想传递“sortby”和“order”的参数:

您可以:

  1. 使用查询字符串,例如。example.com/category/{catid}/item/{itemid}?sortby=itemname&order=asc
  2. 对路径使用mod_rewrite(或类似的):example.com/category/{catid}/item/{itemid}/{sortby}/{order}
  3. 使用与请求一起传递的单个HTTP标头
  4. 使用不同的方法(例如POST)来检索资源。

所有这些都有其缺点,但比使用GET与身体要好得多。

即使一个流行的工具使用这个,正如本页经常引用的那样,我认为这仍然是一个相当糟糕的主意,太异国情调了,尽管规范没有禁止。

许多中间基础设施可能会拒绝此类请求。

例如,忘记在您的网站前使用一些可用的CDN,如下一个

如果查看器GET请求包括主体,CloudFront将HTTP状态代码403(禁止)返回给查看器。

是的,您的客户端库可能也不支持发出此类请求,如本评论所述。

创建请求工厂类

import java.net.URI;
import javax.annotation.PostConstruct;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;import org.apache.http.client.methods.HttpUriRequest;import org.springframework.http.HttpMethod;import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;import org.springframework.stereotype.Component;import org.springframework.web.client.RestTemplate;
@Componentpublic class RequestFactory {private RestTemplate restTemplate = new RestTemplate();
@PostConstructpublic void init() {this.restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestWithBodyFactory());}
private static final class HttpComponentsClientHttpRequestWithBodyFactory extends HttpComponentsClientHttpRequestFactory {@Overrideprotected HttpUriRequest createHttpUriRequest(HttpMethod httpMethod, URI uri) {if (httpMethod == HttpMethod.GET) {return new HttpGetRequestWithEntity(uri);}return super.createHttpUriRequest(httpMethod, uri);}}
private static final class HttpGetRequestWithEntity extends HttpEntityEnclosingRequestBase {public HttpGetRequestWithEntity(final URI uri) {super.setURI(uri);}
@Overridepublic String getMethod() {return HttpMethod.GET.name();}}
public RestTemplate getRestTemplate() {return restTemplate;}}

和@Autow的地方,你需要和使用,这是一个示例代码GET请求与Request estBody

 @RestController@RequestMapping("/v1/API")public class APIServiceController {    
@Autowiredprivate RequestFactory requestFactory;    

@RequestMapping(method = RequestMethod.GET, path = "/getData")public ResponseEntity<APIResponse> getLicenses(@RequestBody APIRequest2 APIRequest){APIResponse response = new APIResponse();HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);Gson gson = new Gson();try {StringBuilder createPartUrl = new StringBuilder(PART_URL).append(PART_URL2);            
HttpEntity<String> entity = new HttpEntity<String>(gson.toJson(APIRequest),headers);ResponseEntity<APIResponse> storeViewResponse = requestFactory.getRestTemplate().exchange(createPartUrl.toString(), HttpMethod.GET, entity, APIResponse.class); //.getForObject(createLicenseUrl.toString(), APIResponse.class, entity);    
if(storeViewResponse.hasBody()) {response = storeViewResponse.getBody();}return new ResponseEntity<APIResponse>(response, HttpStatus.OK);}catch (Exception e) {e.printStackTrace();return new ResponseEntity<APIResponse>(response, HttpStatus.INTERNAL_SERVER_ERROR);}        
}}

GET,有身体!?

在规范方面,你可以,但是,不明智地这样做不是一个好主意,正如我们将看到的那样。

RFC 7231§4.3.1指出主体“没有定义的语义学”,但这并不是说它是禁止的。如果你将一个主体附加到请求中,你的服务器/应用程序从中得到什么取决于你。RFC接着指出GET可以是“各种数据库记录的编程视图”。显然,这种视图多次由大的进审量参数定制,这些参数放在请求目标的查询组件中并不总是方便甚至安全。

好的方面:我喜欢措辞。很明显,读取/获取资源对服务器没有任何可观察到的副作用(该方法是“安全的”),并且,无论第一个请求的结果如何,请求都可以以相同的预期效果重复(该方法是“幂等的”)。

坏处: HTTP/1.1的早期草案禁止GET有主体,据称直到今天,一些实现甚至会丢弃主体、忽略主体或拒绝消息。例如,哑HTTP缓存可能仅从请求目标中构造缓存键,无视主体的存在或内容。更笨的服务器可能会无知到将主体视为新请求,这实际上被称为“请求走私”(即“向一台设备发送请求而另一台设备不知道”的行为)。

由于我认为主要是对实现之间的不可操作性的关注,半成品建议将GET主体归类为“不应该”,“除非[请求]直接向先前已指示的源服务器发出,在带内或带外,这样的请求有目的并将得到充分支持”(强调我的)。

修复:有一些技巧可以用来解决这种方法的一些问题。例如,不感知身体的缓存可以通过简单地将从身体派生的哈希附加到查询组件来间接变得感知身体,或者通过响应来自服务器的cache-control: no-cache标头来完全禁用缓存。

遗憾的是,当涉及到请求链时,人们通常无法控制甚至不知道所有当前和未来的HTTP中介以及它们将如何处理GET主体。这就是为什么这种方法通常被认为是不可靠的。

但是POST不是幂等的!

POST是一种替代方案。POST请求通常包括一个消息正文(只是为了记录,正文不是必需的,参见RFC 7230§3.3.2)。RFC 7231(§4.3.3)中的第一个用例示例是“向数据处理过程提供数据块[…]”。所以就像获取正文一样,后端的正文会发生什么取决于你。

好的方面:当希望发送请求正文时,可能会应用一种更常见的方法,无论出于什么目的,因此,可能会从您的团队成员那里产生最少的噪音(有些人可能仍然错误地认为POST必须创建资源)。

此外,我们经常传递参数的是一个在不断变化的数据上运行的搜索函数,并且只有在响应中提供显式的新鲜度信息时,POST响应才可缓存。

坏处: POST请求未定义为幂等,导致请求重试犹豫。例如,在页面重新加载时,浏览器不愿意在不提示用户不可读的神秘消息的情况下重新提交超文本标记语言表单。

修复:好吧,仅仅因为POST没有被定义为幂等并不意味着它不能是。事实上,RFC 7230§6.3.1写道:“一个用户代理知道(通过设计或配置)对给定资源的POST请求是安全的,可以自动重复该请求”。所以,除非你的客户端是超文本标记语言形式,否则这可能不是一个真正的问题。

QUERY是圣杯

有一个新方法QUERY的提议,它确实为消息主体定义了语义学将该方法定义为幂等的。参见这个

编辑:顺便说一句,我在发现一个代码库后偶然发现了这个StackOverflow问题,在这个代码库中,他们只使用PUT请求来实现服务器端搜索功能。这是他们的想法,包括一个带有参数的主体,并且是幂等的。但是PUT的问题是请求主体有非常精确的语义学。具体来说,PUT“请求创建目标资源的状态或替换为[主体中的]状态”(RFC 7231§4.3.4)。显然,这不包括PUT作为一个可行的选择。

关于一个老问题:

在主体上添加完整的内容,并在查询字符串上添加主体的短哈希,因此缓存不会成为问题(如果主体内容发生变化,哈希将发生变化),并且您将能够在需要时发送大量数据:)

如果您想允许带有主体的GET请求,一种方法是支持带有标头“X-HTTP-方法-覆盖:GET”的POST请求。它在这里描述:https://en.wikipedia.org/wiki/List_of_HTTP_header_fields。这个标头意味着当方法是POST时,请求应该被视为GET。POST允许主体,所以你确定没有人会丢弃GET请求的有效负载。

此标头通常用于通过一些不识别这些方法的代理发出PATCH或HEAD请求,并将它们替换为GET(调试总是很有趣!)。