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

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

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

我的问题:

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

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


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

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

然而,如果你有充分的理由,那就去做吧。

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

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

您试图实现的目标已经用一种更常见的方法完成了很长时间,这种方法不依赖于在GET中使用有效负载。

您可以简单地构建特定的搜索中介类型,或者如果您希望更具RESTful,可以使用类似OpenSearch的方法,并将请求POST到服务器指示的URI,例如/search。然后,服务器可以生成搜索结果或构建最终URI并使用303重定向。

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

也就是说,URI无论如何都是针对非ASCII的任何内容进行编码的,application/x-www-form-urlencoded和multipart/form数据也是如此。如果您打算支持ReSTful场景,我建议使用此格式,而不是创建另一种自定义json格式。

罗伊·菲尔丁(Roy Fielding)关于在GET请求中包含身体的评论。

对换句话说,任何HTTP请求消息都允许包含消息体,因此必须考虑到这一点来解析消息。然而,GET的服务器语义受到限制,使得主体(如果有的话)对请求没有语义意义。解析的需求与方法语义的需求是分开的。所以,是的,你可以用GET发送一个身体,但不,这样做从来都没有用。这是HTTP/1.1分层设计的一部分,一旦规范被划分(正在进行中),分层设计将再次变得清晰。……罗伊

是的,您可以用GET发送请求体,但它应该没有任何意义。如果您通过在服务器上解析它并根据其内容更改响应来赋予其含义,那么您将忽略HTTP/1.1规范第4.3节中的建议:

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

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

GET方法意味着检索请求URI标识的任何信息([…])。

其声明请求主体不是GET请求中的资源标识的一部分,而是请求URI。

使现代化

被称为“HTTP/1.1规范”的RFC2616现在已经过时。2014年,它被RFC7230-7237取代。引号“处理请求时应忽略消息正文”已删除。现在只是“请求消息框架独立于方法语义,即使该方法没有定义消息体的任何用途”第二个引号“GET方法意味着检索任何信息……由请求URI标识”被删除。-来自评论

根据HTTP 1.1 2014规范:

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

不符合base64编码的标头怎么办?“某些参数:sdfSD45fdg45/aS”

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

你可以用身体发送GET,也可以发送POST,然后放弃RESTish宗教信仰(这并不是很糟糕,5年前只有一个信仰的成员——他在上面的评论)。

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

进行POST可能会遇到一些RESTish框架的障碍。

Julian Reschke在上文中建议使用非标准HTTP头,如“SEARCH”,这可能是一个很好的解决方案,只是它不太可能被支持。

列出能够和不能做到上述每一项的客户可能是最有成效的。

无法发送带有正文的GET的客户端(我知道):

XmlHTTPRequest Fiddler

可以发送带有正文的GET的客户端:

大多数浏览器

可以从GET检索正文的服务器和库:

阿帕奇PHP文件

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

?

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

您可能会认为Base64的padding=字符可能对URL的参数值有害,但这似乎不是-请参阅以下讨论:http://mail.python.org/pipermail/python-bugs-list/2007-February/037195.html . 但是,您不应该将编码数据放在没有参数名称的位置,因为带填充的编码字符串将被解释为具有空值的参数键。我会用类似的东西_b64=<编码器数据>。

IMHO,您只需在URL中发送JSON编码(即encodeURIComponent),这样就不会违反HTTP规范并将JSON发送到服务器。

restclient和REST控制台都不支持这一点,但curl支持。

HTTP规范在第4.3节中说明

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

第5.1.1节将我们重定向到第9.x节以了解各种方法。它们都没有明确禁止包含消息体。然而

第5.2节规定

通过检查请求URI和主机头字段来确定由Internet请求标识的确切资源。

第9.3节规定

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

这一起表明,在处理GET请求时,服务器不需要检查除请求URI和主机头字段之外的任何内容。

总之,HTTP规范不会阻止您使用GET发送消息体,但存在足够的歧义,如果不是所有服务器都支持它,我不会感到惊讶。

哪个服务器会忽略它?–2012年8月30日21:27

例如,谷歌做得比忽视它更糟糕,它会认为这是一个错误!

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

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

1234

(1234内容后面是CR-LF,总共6个字节)

你会得到:

HTTP/1.1 400 Bad Request
Server: 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.

您还可以从Bing、Apple等获得400个Bad Request,这些请求由AkamaiGhost提供。

因此,我不建议对主体实体使用GET请求。

Elasticsearch接受带有主体的GET请求。甚至似乎这是首选的方法:Elasticsearch指南

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

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

看看:

ServiceStack中基于消息的设计使用WCF构建基于RESTful消息的Web服务

基于消息的方法可以帮助您解决Get方法的限制。您可以将任何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节“消息正文”:

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

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

这与上面Fielding的引用一致。

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

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

PHP文件:

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

Console命令:

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

输出:

GET
{"the": "body"}

根据XMLHttpRequest,它无效。根据标准:

4.5.6 send()方法客户send([正文=空])启动请求。可选参数提供请求身体如果请求方法是GET或HEAD,则忽略该参数。如果任一状态都不是,则引发InvalidStateError异常打开或设置send()标志。send(body)方法必须运行以下步骤:如果状态未打开,则引发InvalidStateError异常。如果设置了send()标志,则引发InvalidStateError异常。如果请求方法是GET或HEAD,请将body设置为null。如果body为空,则转到下一步。

虽然,我认为这不应该,因为GET请求可能需要大量的正文内容。

因此,如果您依赖浏览器的XMLHttpRequest,它很可能无法工作。

我向IETF HTTP工作组提出了这个问题。Roy Fielding(1998年,http://1.1文档的作者)的评论是

“…一个实现将被破坏,无法执行除解析和丢弃该主体之外的任何操作”

RFC 7213(HTTPbis)规定:

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

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

如果你在GET上包含一个主体,那么有一些代理肯定会以各种方式破坏你的请求。

总之,不要这样做。

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

假设每个类别都有类别和项目。两者都由id标识(在本例中为“catid”/“itemid”)。您希望按照特定“顺序”中的另一个参数“sortby”进行排序。您希望传递“sortby”和“order”的参数:

你可以:

使用查询字符串,例如。example.com/category/{catid}/item/{itemid}?sortby=itemname&order=asc对路径使用mod_rewrite(或类似):示例.com/category/{catid}/item/{itemid}/{sortby}/{order}使用随请求传递的单个HTTP标头使用其他方法(例如POST)检索资源。

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

即使一个流行的工具使用了这个,正如本页经常引用的,我认为这仍然是一个非常糟糕的主意,因为它太奇特了,尽管规范没有禁止。

许多中间基础设施可能只是拒绝这样的请求。

例如,忘记在网站前面使用一些可用的CDN,如下图所示:

如果查看器GET请求包含正文,CloudFront将向查看器返回HTTP状态代码403(禁止)。

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

创建Requestfactory类

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;

@Component
public class RequestFactory {
    private RestTemplate restTemplate = new RestTemplate();

    @PostConstruct
    public void init() {
        this.restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestWithBodyFactory());
    }

    private static final class HttpComponentsClientHttpRequestWithBodyFactory extends HttpComponentsClientHttpRequestFactory {
        @Override
        protected 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);
        }

        @Override
        public String getMethod() {
            return HttpMethod.GET.name();
        }
    }

    public RestTemplate getRestTemplate() {
        return restTemplate;
    }
}

和@Autowired,这里是一个带有RequestBody的GET请求示例代码

 @RestController
 @RequestMapping("/v1/API")
public class APIServiceController {
    
    @Autowired
    private 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);
        }
        
    }
}

得到,用身体!?

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

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

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

糟糕的是:HTTP/1.1的早期草案禁止GET具有主体,而且据称,某些实现直到今天都会丢弃主体、忽略主体或拒绝消息。例如,哑HTTP缓存可能只从请求目标构建缓存密钥,而忽略了主体的存在或内容。更愚蠢的服务器可能会如此无知,以至于将主体视为一个新的请求,这实际上被称为“请求走私”(这是将“请求发送到一个设备,而另一个设备不知道它”的行为-源)。

由于我认为主要关注的是实现之间的不可操作性,正在进行的工作建议将GET主体分类为“不应该”,“除非(请求)直接发送给一个源服务器,该服务器先前已在带内或带外表示此类请求有目的,并且将得到充分支持”(我的重点)。

解决方法:对于这种方法的一些问题,可以使用一些技巧。例如,不知道主体的缓存可以通过简单地将从主体派生的哈希附加到查询组件来间接地变得知道主体,或者通过响应缓存控制(没有来自服务器的缓存头)来完全禁用缓存。

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

但是POST不是幂等的!

POST是一种替代方法。POST请求通常包括一个消息体(仅用于记录,正文不是要求,请参阅RFC 7230§3.3.2)。RFC 7231(§4.3.3)中的第一个用例示例是“向数据处理过程提供数据块[…]”。所以,就像GET与身体一样,身体在后端发生什么取决于你。

好处:当您希望出于任何目的发送请求体时,也许可以采用一种更常见的方法,这样可能会从您的团队成员中产生最少的噪音(有些人可能仍然错误地认为POST必须创建资源)。

此外,我们经常向其传递参数的是一个对不断变化的数据进行操作的搜索函数,只有在响应中提供明确的新鲜度信息时,POST响应才可缓存。

糟糕的是:POST请求没有被定义为幂等,导致请求重试犹豫。例如,在页面重新加载时,浏览器不愿意重新提交HTML表单,而不会向用户提示不可读取的隐藏消息。

解决方法:好吧,POST没有被定义为幂等并不意味着它不一定是幂等的。事实上,RFC 7230§6.3.1写道:“知道(通过设计或配置)对给定资源的POST请求是安全的用户代理可以自动重复该请求”。因此,除非您的客户端是HTML表单,否则这可能不是真正的问题。

QUERY是圣杯

有人提出了一种新的方法QUERY,它确实定义了消息体的语义,并将该方法定义为幂等。看看这个。

编辑:顺便说一句,我在发现了一个代码库后,无意中遇到了这个StackOverflow问题,在该代码库中,他们只将PUT请求用于服务器端搜索功能。这是他们的想法,包括一个带参数的体,并且也是幂等的。遗憾的是,PUT的问题在于请求体具有非常精确的语义。具体来说,PUT“请求创建目标资源的状态或将其替换为[正文]中的状态”(RFC 7231§4.3.4)。显然,这排除了PUT作为一个可行的选项。

关于一个老问题的想法:

在正文中添加完整的内容,在查询字符串中添加正文的短哈希,这样缓存就不会有问题(如果正文内容发生变化,哈希值会发生变化),并且您可以在需要时发送大量数据:)

如果您希望允许带有主体的GET请求,一种方法是支持带有标题“X-HTTP-Method-Override:GET”的POST请求。此处描述如下:https://en.wikipedia.org/wiki/List_of_HTTP_header_fields.这个头意味着当方法是POST时,应该将请求视为GET。POST允许Body,因此您可以确定没有人会丢弃GET请求的有效负载。

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