Cro::HTTP::Client
Cro::HTTP::Client
类提供了一个灵活的 HTTP 和 HTTPS 客户端实现,可以从简单到更复杂的情况进行扩展。它可以通过两种方式消费:
通过对类型对象( Cro::HTTP::Client.get($url)
)进行调用。这对于一次性请求很有用,但在向同一服务器发出多个请求时(例如使用 keep-alive
)不提供连接重用。
通过创建一个 Cro::HTTP::Client
实例。默认情况下,这可以重用连接池。它还可以配置默认的 base URL,传递的默认授权数据,甚至是插入到请求/响应处理管道中的中间件。 Cro::HTTP::Client
实例可以并发地使用。
一般来说,如果您打算发起一次性请求,请使用类型对象。如果您要向同一台服务器或一组服务器发出很多请求,请创建一个实例。
默认情况下,HTTPS 请求将使用 ALPN 来协商是否执行 HTTP/2 或 HTTP/1.1,并且 HTTP 请求将始终使用 HTTP/1.1。
发起基本请求
可以在类型对象或 Cro::HTTP::Client
的实例上调用 get
,post
,put
,delete
,patch
和 head
方法。他们都会返回一个 Promise
,如果请求成功则会被保留(kept), 如果失败则被毁掉(broken)。
my $resp = await Cro::HTTP::Client.get('https://www.raku.org/');
响应($resp
) 是一个 Cro::HTTP::Response
对象。它将在请求头可用时立即生成;请求体可能尚未收到。默认情况下,错误(4xx和5xx状态码)将导致遵守 X::Cro::HTTP::Error
角色的异常,该角色具有包含 Cro::HTTP::Response
对象的 response
属性。
my $resp = await Cro::HTTP::Client.delete($product-url);
CATCH {
when X::Cro::HTTP::Error {
if .response.status == 404 {
say "Product not found!";
}
else {
say "Unexpected error: $_";
}
}
}
实际的异常类型要么是用于 4xx 错误的 X::Cro::HTTP::Error::Client
, 要么是用于 5xx 错误的 X::Cro::HTTP::Error::Server
(这在设置重试时很有用应该区分服务器错误和客户端错误)。
要为每个客户端请求设置 base URL,可以将 base URL 作为 base-uri
参数传递给 Cro::HTTP::Client
实例。
my $client = Cro::HTTP::Client.new(base-uri => "http://persistent.url.com");
await $client.get('/first'); # http://persistent.url.com/first
await $client.get('/another'); # http://persistent.url.com/another
添加额外的请求头
通过给 headers
命名参数传递一个数组,可以为请求设置一个或多个请求头。它可能包含 Pair
对象,Cro::HTTP::Header
实例,或者两者的混合。
my $resp = await Cro::HTTP::Client.get: 'example.com',
headers => [
referer => 'http://anotherexample.com',
Cro::HTTP::Header.new(
name => 'User-agent',
value => 'Cro'
)
];
如果请求头要添加到所有的请求中,则可以在构建时设置默认请求头:
my $client = Cro::HTTP::Client.new:
headers => [
User-agent => 'Cro'
];
设置请求体
为了给请求一个请求体,传递一个 body
命名参数。content-type
命名参数通常也应该传递,以指示正文的类型。例如,具有 JSON 正文的请求可以发送为:
my %panda = name => 'Bao Bao', eats => 'bamboo';
my $resp = await Cro::HTTP::Client.post: 'we.love.pand.as/pandas',
content-type => 'application/json',
body => %panda;
如果为 JSON API 编写客户端,那么在每个请求上设置内容类型(content type)可能会变得繁琐。在这种情况下,它可以在构建客户端实例时设置,并在默认情况下使用(请注意,只有在设置了主体的情况下才会使用它):
# Configure with JSON content type.
my $client = Cro::HTTP::Client.new: content-type => 'application/json';
# And later get it added by default.
my %panda = name => 'Bao Bao', eats => 'bamboo';
my $resp = await $client.post: 'we.love.pand.as/pandas', body => %panda;
Cro::HTTP::Client
类使用一个 Cro::BodySerializer
来序列化所发送的请求体。除了 JSON 之外,还有 body parser 编码和发送一个 Str
:
my $resp = await Cro::HTTP::Client.post: 'we.love.pand.as/facts',
content-type => 'text/plain; charset=UTF-8',
body => "99% of a Panda's diet consists of bamboo";
Blob
:
my $resp = await Cro::HTTP::Client.put: 'we.love.pand.as/images/baobao.jpg',
content-type => 'image/jpeg',
body => slurp('baobao.jpg', :bin);
根据 application/x-www-form-urlencoded
格式化的表单数据(这是 Web 浏览器中的默认设置):
my $resp = await Cro::HTTP::Client.post: 'we.love.pand.as/pandas',
content-type => 'application/x-www-form-urlencoded',
# Can use a Hash; an Array of Pair allows multiple values per name
body => [
name => 'Bao Bao',
eats => 'bamboo'
];
或者根据 multipart/form-data
格式化表单数据(这在Web浏览器中用于包含文件上传的表单):
my $resp = await Cro::HTTP::Client.post: 'we.love.pand.as/pandas',
content-type => 'multipart/form-data',
body => [
# Simple pairs for simple form values
name => 'Bao Bao',
eats => 'bamboo',
# For file uploads, make a part object
Cro::HTTP::Body::MultiPartFormData::Part.new(
headers => [Cro::HTTP::Header.new(
name => 'Content-type',
value => 'image/jpeg'
)],
name => 'photo',
filename => 'baobao.jpg',
body-blob => slurp('baobao.jpg', :bin)
)
];
要替换客户端将使用的一组正文序列化程序,请在构造 Cro::HTTP::Client
实例时给 body-serializers
命名参数传递一个数组:
my $client = Cro::HTTP::Client.new:
body-serializers => [
Cro::HTTP::BodySerializer::JSON,
My::BodySerializer::XML
];
要改为保留现有的一组正文序列化器并添加一些新的(它们将具有较高的优先级),请使用 add-body-serializers
:
my $client = Cro::HTTP::Client.new:
add-body-serializers => [ My::BodySerializer::XML ];
通过将一个 Supply
传递给 body-byte-stream
,也可以让主体来自一个字节流。
my $resp = await Cro::HTTP::Client.post: 'example.com/incoming',
content-type => 'application/octet-stream',
body-byte-stream => $supply;
body
和 body-byte-stream
参数不能一起使用; 试图这样做会导致 X::Cro::HTTP::Client::BodyAlreadySet
异常.
获得响应体
响应体通常由 Promise
(如果请求整个主体)或 Supply
(当主体到达时将交付)异步提供的。
body
方法返回一个 Promise
,当接收并分析正文时将保留 Promise。一些默认的主体解析器是:
-
JSON,当
Content-type
头部是application/json
或使用+json
后缀时将使用这个 JSON。JSON::Fast
将用于执行解析。 -
字符串回退,在
Content-type
为text
时使用。会返回一个Str
。 -
Blob 回退,在所有其他情况下使用,并返回一个带主体的
Blob
。
一个 Cro::HTTP::Client
可以通过传递 body-parsers
命名参数来配置一个替代的 body 解析器组:
my $client = Cro::HTTP::Client.new:
body-parsers => [
Cro::HTTP::BodyParser::JSON,
My::BodyParser::XML
];
或者使用 add-body-parsers
在默认设置的顶部添加额外的 body 解析器:
my $client = Cro::HTTP::Client.new:
add-body-parsers => [ My::BodyParser::XML ];
为了将响应主体作为一个 Supply
来供应,当它们通过网络到达时会发送这些字节,请使用 body-byte-stream
方法:
react {
whenever $resp.body-byte-stream -> $chunk {
say "Got chunk: $chunk.gist()";
}
}
要将整个响应主体作为 Blob
获取,请使用 body-blob
方法:
my Blob $body = await $resp.body-blob();
要将整个响应主体作为 Str
,请使用 body-text
方法:
my Str $body = await $resp.body-text();
此方法将查看 Content-type
头以查看是否指定了 charset
,并使用该字符串解码身体。否则,它会查看主体是否以 BOM 开始并依赖于此。如果没有通过,将使用启发式:如果 body 可以被解码为 utf-8,那么它将被认为是 utf-8,并且如果它不能被解码为 latin-1(它永远不会失败因为所有字节都是有效的)。
Cookies
默认情况下,响应中的 Cookie 会被忽略。但是,使用 :cookie-jar
选项(即传递 True
)构造一个 Cro::HTTP::Client
将创建一个 Cro::HTTP::Client::CookieJar
实例。这将用于存储响应中设置的所有 Cookie。相关的 cookies 将自动包含在后续请求中。
my $client = Cro::HTTP::Client.new(:cookie-jar);
Cookie 相关性通过考虑主机,路径和 Secure
扩展来确定。已经过了最大年龄的过期日期的 Cookies 将自动从 cookie jar 中移除。
也可以传入一个 Cro::HTTP::Client::CookieJar
的实例,这样就可以在客户端的几个实例中共享一个 cookie jar(或者传入添加额外功能的子类)。
my $jar = Cro::HTTP::Client::CookieJar.new;
my $client = Cro::HTTP::Client.new(cookie-jar => $jar);
my $json-client = Cro::HTTP::Client.new:
cookie-jar => $jar,
content-type => 'application/json';
要在请求中包含一组特定的 cookie,请在进行reuqest请求时使用 cookies
命名参数将它们传递给哈希:
my $resp = await $client.get: 'http://somesite.com/',
cookies => {
session => $fake-session-id
};
以这种方式传递的 Cookie 将覆盖 cookie jar 中的任何 cookie。
要获取响应设置的 cookie,请在 Cro::HTTP::Response
对象上使用 cookie 方法,该方法返回一个 Cro::HTTP::Cookie
对象列表。
重定向
默认情况下,Cro::HTTP::Client
将遵循 HTTP 重定向响应,强制执行 5 个重定向限制以避免循环重定向。如果有 5 个以上的重定向,X::Cro::HTTP::Client::TooManyRedirects
将被抛出。
可以在构建新的 Cro::HTTP::Client
时或在每个请求的基础上配置此行为,并且每个请求设置都会覆盖在构建时配置的行为。无论哪种情况,都是使用后面的命名参数完成的。
:follow # follow redirects (up to 5 times per request)
:!follow # never follow redirects
:follow(2) # follow redirects (up to 2 times per request)
:follow(10) # follow redirects (up to 10 times per request)
目前 301,307 和 308 重定向的处理方式相同,永久重定向不会缓存。他们保留原始的请求方法。不管最初的请求方法如何,302 和 303 会导致发出 GET 请求。
认证
基本认证和承载认证均由 Cro::HTTP::Client
直接支持。这些可以在实例化客户端或每个请求(将覆盖实例上配置的)时进行配置。
对于基本身份验证,请将 auth
选项与包含用户名和密码的哈希值一起传递。
auth => {
username => $user,
password => $password
}
对于承载认证,传递一个包含承载的 auth
散列选项:
auth => { bearer => $jwt }
未能正确传递用户名和密码或承载将导致 X::Cro::Client::InvalidAuth
异常。
在这两种情况下,认证信息将随请求立即发送。为了仅在服务器以 401 响应响应初始请求时才发送它,请将 if-ask
选项设置为 True
。
auth => {
username => $user,
password => $password,
if-asked => True
}
持久化连接
Cro::HTTP::Client
实例默认使用持久化连接。当对同一台服务器发起多个请求时,可以通过不需要每次建立新连接来实现更高的吞吐量。要不使用持久连接,请将 :!persistent
传递给构造函数。当使用类型对象(例如,Cro::HTTP::Client.get($url)
时,将不会使用持久连接缓存.
HTTP 版本
可以在构造函数或发起每个请求时传递 :http
选项来控制应该使用哪个版本的 HTTP。它可以传递单个项目或列表。有效的选项是 1.1
(它也会隐式地处理 HTTP/1.0)和 2。
:http<1.1> # HTTP/1.1 only
:http<2> # HTTP/2 only
:http<1.1 2> # HTTP/1.1 and HTTP/2 (HTTPS only; selected by ALPN)
对于 HTTP 请求,默认值为:http <1.1>,对于HTTPS请求,默认值为:http <1.1 2>。使用HTTP <1.1 2>与HTTP连接是不合法的,因为ALPN是决定使用哪种协议的唯一支持机制。
Push promises
HTTP/2.0
支持推送 promises,它允许服务器将额外资源作为响应的一部分推送给客户端。默认情况下,Cro::HTTP::Client
将指示远程服务器不发送 push promises。要加入此功能,可以选择:
-
如果创建一个
Cro::HTTP::Client
实例,请将:push-promises
传递给构造函数,以便为使用客户端实例发起的所有请求启用它们。 -
否则,在发出请求时传递
:push-promises
(例如,get
方法)。但是,在使用 HTTP/2.0 时,通常很明智的做法是创建一个实例并重用连接来处理多个请求。
通过调用请求产生的 Cro::HTTP::Response
对象的 push-promises
方法来获得 Push promise。这将返回一个 Supply
,它会为服务器发送的每个 push promise 发出一个 Cro::HTTP::PushPromise
实例。其中每个人都有一 response
属性,返回一个 Promise
,当 push promise 被满足时,Promise
将被保存在一个 Cro::HTTP::Response
对象中。
因此可以实现请求并获得所有 push promises,如下所示:
react {
my $client = Cro::HTTP::Client.new(:push-promises);
my $response = await $client.get($url);
whenever $response.push-promises -> $prom {
whenever $prom.response -> $resp {
say "Push promise for $prom.target() had status $resp.status()";
}
}
}