Cro::HTTP::Client

HTTP 客户端

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 的实例上调用 getpostputdeletepatchhead 方法。他们都会返回一个 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;

bodybody-byte-stream 参数不能一起使用; 试图这样做会导致 X::Cro::HTTP::Client::BodyAlreadySet 异常.

获得响应体

响应体通常由 Promise(如果请求整个主体)或 Supply(当主体到达时将交付)异步提供的。

body 方法返回一个 Promise,当接收并分析正文时将保留 Promise。一些默认的主体解析器是:

  • JSON,当 Content-type 头部是 application/json 或使用 +json 后缀时将使用这个 JSON。 JSON::Fast 将用于执行解析。

  • 字符串回退,在 Content-typetext 时使用。会返回一个 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()";
        }
    }
}
Cro 

comments powered by Disqus