File Coverage

blib/lib/Mojo/UserAgent/Transactor.pm
Criterion Covered Total %
statement 225 225 100.0
branch 123 130 94.6
condition 67 78 85.9
subroutine 33 33 100.0
pod 10 10 100.0
total 458 476 96.2


line stmt bran cond sub pod time code
1             package Mojo::UserAgent::Transactor;
2 58     58   1024 use Mojo::Base -base;
  58         142  
  58         542  
3              
4 58     58   519 use Mojo::Asset::File;
  58         162  
  58         2464  
5 58     58   371 use Mojo::Asset::Memory;
  58         182  
  58         500  
6 58     58   1273 use Mojo::Content::MultiPart;
  58         360  
  58         667  
7 58     58   716 use Mojo::Content::Single;
  58         455  
  58         670  
8 58     58   364 use Mojo::File qw(path);
  58         142  
  58         4234  
9 58     58   409 use Mojo::JSON qw(encode_json);
  58         249  
  58         3276  
10 58     58   411 use Mojo::Parameters;
  58         126  
  58         612  
11 58     58   34313 use Mojo::Transaction::HTTP;
  58         210  
  58         508  
12 58     58   448 use Mojo::Transaction::WebSocket;
  58         145  
  58         446  
13 58     58   329 use Mojo::URL;
  58         554  
  58         426  
14 58     58   348 use Mojo::Util qw(encode url_escape);
  58         388  
  58         4435  
15 58     58   401 use Mojo::WebSocket qw(challenge client_handshake);
  58         121  
  58         303816  
16              
17             has compressed => sub { $ENV{MOJO_GZIP} // 1 };
18             has generators => sub { {form => \&_form, json => \&_json, multipart => \&_multipart} };
19             has name => 'Mojolicious (Perl)';
20              
21 1 50   1 1 15 sub add_generator { $_[0]->generators->{$_[1]} = $_[2] and return $_[0] }
22              
23             sub download {
24 22     22 1 76 my ($self, $head, $path) = @_;
25              
26 22         81 my $req = $head->req;
27 22         73 my $tx = $self->tx(GET => $req->url->clone => $req->headers->to_hash);
28 22         132 my $res = $tx->res;
29 22 50       106 if (my $error = $head->error) { $res->error($error) and return $tx }
  2 100       8  
30              
31 20         78 my $headers = $head->res->headers;
32 20   100     71 my $accept_ranges = ($headers->accept_ranges // '') =~ /bytes/;
33 20   100     70 my $size = $headers->content_length // 0;
34              
35 20         42 my $current_size = 0;
36 20         111 my $file = path($path);
37 20 100       115 if (-f $file) {
38 12         62 $current_size = -s $file;
39 12 100 50     96 $res->error({message => 'Unknown file size'}) and return $tx unless $size;
40 10 100 50     59 $res->error({message => 'File size mismatch'}) and return $tx if $current_size > $size;
41 8 100 50     40 $res->error({message => 'Download complete'}) and return $tx if $current_size == $size;
42 6 100 50     33 $res->error({message => 'Server does not support partial requests'}) and return $tx unless $accept_ranges;
43 4         19 $tx->req->headers->range("bytes=$current_size-$size");
44             }
45              
46 12         72 my $fh = $file->open('>>');
47             $res->content->unsubscribe('read')->on(
48             read => sub {
49 10     10   27 my ($content, $bytes) = @_;
50 10         35 $current_size += length $bytes;
51 10 50       51 $fh->syswrite($bytes) == length $bytes or $res->error({message => qq/Can't write to file "$path": $!/});
52             }
53 12         69 );
54             $res->on(
55             finish => sub {
56 9     9   17 my $res = shift;
57 9 100       51 $res->error({message => 'Download incomplete'}) if $current_size < $size;
58             }
59 12         83 );
60              
61 12         75 return $tx;
62             }
63              
64             sub endpoint {
65 2190     2190 1 5882 my ($self, $tx) = @_;
66              
67             # Basic endpoint
68 2190         7301 my $req = $tx->req;
69 2190         10531 my $url = $req->url;
70 2190   50     10260 my $proto = $url->protocol || 'http';
71 2190         8867 my $host = $url->ihost;
72 2190 100 66     7202 my $port = $url->port // ($proto eq 'https' ? 443 : 80);
73              
74             # Proxy for normal HTTP requests
75 2190         8254 my $socks;
76 2190 100       7834 if (my $proxy = $req->proxy) { $socks = $proxy->protocol eq 'socks' }
  64         145  
77 2190 100 100     12059 return _proxy($tx, $proto, $host, $port) if $proto eq 'http' && !$req->is_handshake && !$socks;
      100        
78              
79 206         1200 return $proto, $host, $port;
80             }
81              
82 255     255 1 1007 sub peer { _proxy($_[1], $_[0]->endpoint($_[1])) }
83              
84             sub promisify {
85 47     47 1 130 my ($self, $promise, $tx) = @_;
86 47         141 my $err = $tx->error;
87 47 100 100     351 return $promise->reject($err->{message}) if $err && !$err->{code};
88 43 100 100     109 return $promise->reject('WebSocket handshake failed') if $tx->req->is_handshake && !$tx->is_websocket;
89 41         223 $promise->resolve($tx);
90             }
91              
92             sub proxy_connect {
93 232     232 1 2209 my ($self, $old) = @_;
94              
95             # Already a CONNECT request
96 232         729 my $req = $old->req;
97 232 100       814 return undef if uc $req->method eq 'CONNECT';
98              
99             # No proxy
100 228 100 100     1550 return undef unless (my $proxy = $req->proxy) && $req->via_proxy;
101 7 100       26 return undef if $proxy->protocol eq 'socks';
102              
103             # WebSocket and/or HTTPS
104 5         14 my $url = $req->url;
105 5 100 100     16 return undef unless $req->is_handshake || $url->protocol eq 'https';
106              
107             # CONNECT request (expect a bad response)
108 3         17 my $new = $self->tx(CONNECT => $url->clone->userinfo(undef));
109 3         11 $new->req->proxy($proxy);
110 3         11 $new->res->content->auto_relax(0)->headers->connection('keep-alive');
111              
112 3         21 return $new;
113             }
114              
115             sub redirect {
116 971     971 1 4849 my ($self, $old) = @_;
117              
118             # Commonly used codes
119 971         3315 my $res = $old->res;
120 971   100     2826 my $code = $res->code // 0;
121 971 100       2734 return undef unless grep { $_ == $code } 301, 302, 303, 307, 308;
  4855         17457  
122              
123             # CONNECT requests cannot be redirected
124 58         224 my $req = $old->req;
125 58 100       227 return undef if uc $req->method eq 'CONNECT';
126              
127             # Fix location without authority and/or scheme
128 57 50       218 return undef unless my $location = $res->headers->every_header('Location')->[0];
129 57         308 $location = Mojo::URL->new($location);
130 57 100       262 $location = $location->base($req->url)->to_abs unless $location->is_abs;
131 57         297 my $proto = $location->protocol;
132 57 100 100     411 return undef if ($proto ne 'http' && $proto ne 'https') || !$location->host;
      100        
133              
134             # Clone request if necessary
135 55         266 my $new = Mojo::Transaction::HTTP->new;
136 55 100 100     3015 if ($code == 307 || $code == 308) {
137 10 100       64 return undef unless my $clone = $req->clone;
138 8         34 $new->req($clone);
139             }
140             else {
141 45         157 my $method = uc $req->method;
142 45 100 100     302 $method = $code == 303 || $method eq 'POST' ? 'GET' : $method;
143 45         205 $new->req->method($method)->content->headers(my $headers = $req->headers->clone);
144 45         111 $headers->remove($_) for grep {/^content-/i} @{$headers->names};
  153         439  
  45         158  
145             }
146              
147 53         203 my $content = $new->res->content;
148 53 100       224 $content->auto_decompress(0) unless $self->compressed;
149 53         192 my $headers = $new->req->url($location)->headers;
150 53         340 $headers->remove($_) for qw(Authorization Cookie Host Referer);
151 53 100       163 if ($res->content->has_subscribers('sse')) { $content->on(sse => $_) for @{$res->content->subscribers('sse')} }
  1         4  
  1         4  
152              
153 53         243 return $new->previous($old);
154             }
155              
156             sub tx {
157 1128     1128 1 324913 my ($self, $method, $url) = (shift, shift, shift);
158              
159             # Method and URL
160 1128         7918 my $tx = Mojo::Transaction::HTTP->new;
161 1128         5872 my $req = $tx->req->method($method);
162 1128 100       5965 ref $url ? $req->url($url) : $req->url->parse($url =~ m!^/|://! ? $url : "http://$url");
    100          
163              
164             # Headers (we identify ourselves and accept gzip compression)
165 1128         5813 my $headers = $req->headers;
166 1128 100       5899 $headers->from_hash(shift) if ref $_[0] eq 'HASH';
167 1128 100       5093 $headers->user_agent($self->name) unless $headers->user_agent;
168 1128 100       5126 if (!$self->compressed) { $tx->res->content->auto_decompress(0) }
  3 100       10  
169 1103         3296 elsif (!$headers->accept_encoding) { $headers->accept_encoding('gzip') }
170              
171             # Generator
172 1128 100       5984 if (@_ > 1) {
    100          
173 83         353 my $cb = $self->generators->{shift()};
174 83         937 $self->$cb($tx, @_);
175             }
176              
177             # Body
178 25         147 elsif (@_) { $req->body(shift) }
179              
180 1128         8509 return $tx;
181             }
182              
183             sub upgrade {
184 1022     1022 1 3066 my ($self, $tx) = @_;
185 1022   100     3408 my $code = $tx->res->code // 0;
186 1022 100 100     3629 return undef unless $tx->req->is_handshake && $code == 101;
187 75         428 my $ws = Mojo::Transaction::WebSocket->new(handshake => $tx, masked => 1);
188 75 50       398 return challenge($ws) ? $ws->established(1) : undef;
189             }
190              
191             sub websocket {
192 93     93 1 37547 my $self = shift;
193              
194             # New WebSocket transaction
195 93 100       492 my $sub = ref $_[-1] eq 'ARRAY' ? pop : [];
196 93         489 my $tx = $self->tx(GET => @_);
197 93         346 my $req = $tx->req;
198 93 100       310 $req->headers->sec_websocket_protocol(join ', ', @$sub) if @$sub;
199              
200             # Handshake protocol
201 93         302 my $url = $req->url;
202 93   50     438 my $proto = $url->protocol // '';
203 93 100       560 if ($proto eq 'ws') { $url->scheme('http') }
  7 100       47  
    100          
204 5         13 elsif ($proto eq 'wss') { $url->scheme('https') }
205 1         4 elsif ($proto eq 'ws+unix') { $url->scheme('http+unix') }
206              
207 93         455 return client_handshake $tx;
208             }
209              
210 31     31   277 sub _content { Mojo::Content::MultiPart->new(headers => $_[0], parts => $_[1]) }
211              
212             sub _form {
213 66     66   245 my ($self, $tx, $form, %options) = @_;
214 66 100       370 $options{charset} = 'UTF-8' unless exists $options{charset};
215              
216             # Check for uploads and force multipart if necessary
217 66         228 my $req = $tx->req;
218 66         279 my $headers = $req->headers;
219 66   100     247 my $multipart = ($headers->content_type // '') =~ m!multipart/form-data!i;
220 66 100       265 for my $value (map { ref $_ eq 'ARRAY' ? @$_ : $_ } values %$form) {
  104         553  
221 110 100 50     395 ++$multipart and last if ref $value eq 'HASH';
222             }
223              
224             # Multipart
225 66 100       252 if ($multipart) {
226 27         123 $req->content(_content($headers, _form_parts($options{charset}, $form)));
227 27         97 _type($headers, 'multipart/form-data');
228 27         96 return $tx;
229             }
230              
231             # Query parameters or urlencoded
232 39         146 my $method = uc $req->method;
233 39         184 my @form = map { $_ => $form->{$_} } sort keys %$form;
  63         200  
234 39 100 100     260 if ($method eq 'GET' || $method eq 'HEAD') { $req->url->query->merge(@form) }
  7         34  
235             else {
236 32         254 $req->body(Mojo::Parameters->new(@form)->charset($options{charset})->to_string);
237 32         243 _type($headers, 'application/x-www-form-urlencoded');
238             }
239              
240 39         165 return $tx;
241             }
242              
243             sub _form_parts {
244 27     27   81 my ($charset, $form) = @_;
245              
246 27         52 my @parts;
247 27         109 for my $name (sort keys %$form) {
248 41 100       297 next unless defined(my $values = $form->{$name});
249 40 100       160 $values = [$values] unless ref $values eq 'ARRAY';
250 40         81 push @parts, @{_parts($charset, $name, $values)};
  40         113  
251             }
252              
253 27         109 return \@parts;
254             }
255              
256             sub _json {
257 11     11   37 my ($self, $tx, $data) = @_;
258 11         66 _type($tx->req->body(encode_json $data)->headers, 'application/json');
259 11         35 return $tx;
260             }
261              
262             sub _multipart {
263 4     4   9 my ($self, $tx, $parts) = @_;
264 4         12 my $req = $tx->req;
265 4         13 $req->content(_content($req->headers, _parts(undef, undef, $parts)));
266 4         31 return $tx;
267             }
268              
269             sub _parts {
270 44     44   129 my ($charset, $name, $values) = @_;
271              
272 44         83 my @parts;
273 44         102 for my $value (@$values) {
274 55         239 push @parts, my $part = Mojo::Content::Single->new;
275              
276 55         104 my $filename;
277 55         149 my $headers = $part->headers;
278 55 100       159 if (ref $value eq 'HASH') {
279              
280             # File
281 30 100       143 if (my $file = delete $value->{file}) {
    50          
282 9 100       70 $file = Mojo::Asset::File->new(path => $file) unless ref $file;
283 9         32 $part->asset($file);
284 9 100 66     123 $value->{filename} //= path($file->path)->basename if $file->isa('Mojo::Asset::File');
285             }
286              
287             # Memory
288             elsif (defined(my $content = delete $value->{content})) {
289 21         88 $part->asset(Mojo::Asset::Memory->new->add_chunk($content));
290             }
291              
292             # Filename and headers
293 30         99 $filename = delete $value->{filename};
294 30         148 $headers->from_hash($value);
295 30 100       82 next unless defined $name;
296 26   66     190 $filename = url_escape $filename // $name, '"';
297 26 50       191 $filename = encode $charset, $filename if $charset;
298             }
299              
300             # Field
301             else {
302 25 100       147 $value = encode $charset, $value if $charset;
303 25         128 $part->asset(Mojo::Asset::Memory->new->add_chunk($value));
304             }
305              
306             # Content-Disposition
307 51 100 100     315 next if !defined $name || defined $headers->content_disposition;
308 48         152 $name = url_escape $name, '"';
309 48 100       239 $name = encode $charset, $name if $charset;
310 48         117 my $disposition = qq{form-data; name="$name"};
311 48 100       148 $disposition .= qq{; filename="$filename"} if defined $filename;
312 48         141 $headers->content_disposition($disposition);
313             }
314              
315 44         210 return \@parts;
316             }
317              
318             sub _proxy {
319 2239     2239   7971 my ($tx, $proto, $host, $port) = @_;
320              
321 2239         6822 my $req = $tx->req;
322 2239 100 100     8165 if ($req->via_proxy && (my $proxy = $req->proxy)) {
323 51 100 66     104 return $proxy->protocol, $proxy->ihost, $proxy->port // ($proto eq 'https' ? 443 : 80);
324             }
325              
326 2188         16306 return $proto, $host, $port;
327             }
328              
329 70 100   70   302 sub _type { $_[0]->content_type($_[1]) unless $_[0]->content_type }
330              
331             1;
332              
333             =encoding utf8
334              
335             =head1 NAME
336              
337             Mojo::UserAgent::Transactor - User agent transactor
338              
339             =head1 SYNOPSIS
340              
341             use Mojo::UserAgent::Transactor;
342              
343             # GET request with Accept header
344             my $t = Mojo::UserAgent::Transactor->new;
345             say $t->tx(GET => 'http://example.com' => {Accept => '*/*'})->req->to_string;
346              
347             # POST request with form-data
348             say $t->tx(POST => 'example.com' => form => {a => 'b'})->req->to_string;
349              
350             # PUT request with JSON data
351             say $t->tx(PUT => 'example.com' => json => {a => 'b'})->req->to_string;
352              
353             =head1 DESCRIPTION
354              
355             L is the transaction building and manipulation framework used by L.
356              
357             =head1 GENERATORS
358              
359             These content generators are available by default.
360              
361             =head2 form
362              
363             $t->tx(POST => 'http://example.com' => form => {a => 'b'});
364              
365             Generate query string, C or C content. See L for more.
366              
367             =head2 json
368              
369             $t->tx(PATCH => 'http://example.com' => json => {a => 'b'});
370              
371             Generate JSON content with L. See L for more.
372              
373             =head2 multipart
374              
375             $t->tx(PUT => 'http://example.com' => multipart => ['Hello', 'World!']);
376              
377             Generate multipart content. See L for more.
378              
379             =head1 ATTRIBUTES
380              
381             L implements the following attributes.
382              
383             =head2 compressed
384              
385             my $bool = $t->compressed;
386             $t = $t->compressed($bool);
387              
388             Try to negotiate compression for the response content and decompress it automatically, defaults to the value of the
389             C environment variable or true.
390              
391             =head2 generators
392              
393             my $generators = $t->generators;
394             $t = $t->generators({foo => sub {...}});
395              
396             Registered content generators, by default only C
, C and C are already defined.
397              
398             =head2 name
399              
400             my $name = $t->name;
401             $t = $t->name('Mojolicious');
402              
403             Value for C request header of generated transactions, defaults to C.
404              
405             =head1 METHODS
406              
407             L inherits all methods from L and implements the following new ones.
408              
409             =head2 add_generator
410              
411             $t = $t->add_generator(foo => sub {...});
412              
413             Register a content generator.
414              
415             $t->add_generator(foo => sub ($t, $tx, @args) {...});
416              
417             =head2 download
418              
419             my $tx = $t->download(Mojo::Transaction::HTTP->new, '/home/sri/test.tar.gz');
420              
421             Build L resumable file download request as follow-up to a C request. Note that this
422             method is B and might change without warning!
423              
424             =head2 endpoint
425              
426             my ($proto, $host, $port) = $t->endpoint(Mojo::Transaction::HTTP->new);
427              
428             Actual endpoint for transaction.
429              
430             =head2 peer
431              
432             my ($proto, $host, $port) = $t->peer(Mojo::Transaction::HTTP->new);
433              
434             Actual peer for transaction.
435              
436             =head2 promisify
437              
438             $t->promisify(Mojo::Promise->new, Mojo::Transaction::HTTP->new);
439              
440             Resolve or reject L object with L object.
441              
442             =head2 proxy_connect
443              
444             my $tx = $t->proxy_connect(Mojo::Transaction::HTTP->new);
445              
446             Build L proxy C request for transaction if possible.
447              
448             =head2 redirect
449              
450             my $tx = $t->redirect(Mojo::Transaction::HTTP->new);
451              
452             Build L follow-up request for C<301>, C<302>, C<303>, C<307> or C<308> redirect response if
453             possible.
454              
455             =head2 tx
456              
457             my $tx = $t->tx(GET => 'example.com');
458             my $tx = $t->tx(POST => 'http://example.com');
459             my $tx = $t->tx(GET => 'http://example.com' => {Accept => '*/*'});
460             my $tx = $t->tx(PUT => 'http://example.com' => 'Content!');
461             my $tx = $t->tx(PUT => 'http://example.com' => form => {a => 'b'});
462             my $tx = $t->tx(PUT => 'http://example.com' => json => {a => 'b'});
463             my $tx = $t->tx(PUT => 'https://example.com' => multipart => ['a', 'b']);
464             my $tx = $t->tx(POST => 'example.com' => {Accept => '*/*'} => 'Content!');
465             my $tx = $t->tx(PUT => 'example.com' => {Accept => '*/*'} => form => {a => 'b'});
466             my $tx = $t->tx(PUT => 'example.com' => {Accept => '*/*'} => json => {a => 'b'});
467             my $tx = $t->tx(PUT => 'example.com' => {Accept => '*/*'} => multipart => ['a', 'b']);
468              
469             Versatile general purpose L transaction builder for requests, with support for
470             L.
471              
472             # Generate and inspect custom GET request with DNT header and content
473             say $t->tx(GET => 'example.com' => {DNT => 1} => 'Bye!')->req->to_string;
474              
475             # Stream response content to STDOUT
476             my $tx = $t->tx(GET => 'http://example.com');
477             $tx->res->content->unsubscribe('read')->on(read => sub { say $_[1] });
478              
479             # PUT request with content streamed from file
480             my $tx = $t->tx(PUT => 'http://example.com');
481             $tx->req->content->asset(Mojo::Asset::File->new(path => '/foo.txt'));
482              
483             The C content generator uses L for encoding and sets the content type to C.
484              
485             # POST request with "application/json" content
486             my $tx = $t->tx(POST => 'http://example.com' => json => {a => 'b', c => [1, 2, 3]});
487              
488             The C content generator will automatically use query parameters for C and C requests.
489              
490             # GET request with query parameters
491             my $tx = $t->tx(GET => 'http://example.com' => form => {a => 'b'});
492              
493             For all other request methods the C content type is used.
494              
495             # POST request with "application/x-www-form-urlencoded" content
496             my $tx = $t->tx(POST => 'http://example.com' => form => {a => 'b', c => 'd'});
497              
498             Parameters may be encoded with the C option.
499              
500             # PUT request with Shift_JIS encoded form values
501             my $tx = $t->tx(PUT => 'example.com' => form => {a => 'b'} => charset => 'Shift_JIS');
502              
503             An array reference can be used for multiple form values sharing the same name.
504              
505             # POST request with form values sharing the same name
506             my $tx = $t->tx(POST => 'http://example.com' => form => {a => ['b', 'c', 'd']});
507              
508             A hash reference with a C or C value can be used to switch to the C content type
509             for file uploads.
510              
511             # POST request with "multipart/form-data" content
512             my $tx = $t->tx(POST => 'http://example.com' => form => {mytext => {content => 'lala'}});
513              
514             # POST request with multiple files sharing the same name
515             my $tx = $t->tx(POST => 'http://example.com' => form => {mytext => [{content => 'first'}, {content => 'second'}]});
516              
517             The C value should contain the path to the file you want to upload or an asset object, like L
518             or L.
519              
520             # POST request with upload streamed from file
521             my $tx = $t->tx(POST => 'http://example.com' => form => {mytext => {file => '/foo.txt'}});
522              
523             # POST request with upload streamed from asset
524             my $asset = Mojo::Asset::Memory->new->add_chunk('lalala');
525             my $tx = $t->tx(POST => 'http://example.com' => form => {mytext => {file => $asset}});
526              
527             A C value will be generated automatically, but can also be set manually if necessary. All remaining values in
528             the hash reference get merged into the C content as headers.
529              
530             # POST request with form values and customized upload (filename and header)
531             my $tx = $t->tx(POST => 'http://example.com' => form => {
532             a => 'b',
533             c => 'd',
534             mytext => {
535             content => 'lalala',
536             filename => 'foo.txt',
537             'Content-Type' => 'text/plain'
538             }
539             });
540              
541             The C content type can also be enforced by setting the C header manually.
542              
543             # Force "multipart/form-data"
544             my $headers = {'Content-Type' => 'multipart/form-data'};
545             my $tx = $t->tx(POST => 'example.com' => $headers => form => {a => 'b'});
546              
547             The C content generator can be used to build custom multipart requests and does not set a content type.
548              
549             # POST request with multipart content ("foo" and "bar")
550             my $tx = $t->tx(POST => 'http://example.com' => multipart => ['foo', 'bar']);
551              
552             Similar to the C content generator you can also pass hash references with C or C values, as well
553             as headers.
554              
555             # POST request with multipart content streamed from file
556             my $tx = $t->tx(POST => 'http://example.com' => multipart => [{file => '/foo.txt'}]);
557              
558             # PUT request with multipart content streamed from asset
559             my $headers = {'Content-Type' => 'multipart/custom'};
560             my $asset = Mojo::Asset::Memory->new->add_chunk('lalala');
561             my $tx = $t->tx(PUT => 'http://example.com' => $headers => multipart => [{file => $asset}]);
562              
563             # POST request with multipart content and custom headers
564             my $tx = $t->tx(POST => 'http://example.com' => multipart => [
565             {
566             content => 'Hello',
567             'Content-Type' => 'text/plain',
568             'Content-Language' => 'en-US'
569             },
570             {
571             content => 'World!',
572             'Content-Type' => 'text/plain',
573             'Content-Language' => 'en-US'
574             }
575             ]);
576              
577             =head2 upgrade
578              
579             my $tx = $t->upgrade(Mojo::Transaction::HTTP->new);
580              
581             Build L follow-up transaction for WebSocket handshake if possible.
582              
583             =head2 websocket
584              
585             my $tx = $t->websocket('ws://example.com');
586             my $tx = $t->websocket('ws://example.com' => {DNT => 1} => ['v1.proto']);
587              
588             Versatile L transaction builder for WebSocket handshake requests.
589              
590             =head1 SEE ALSO
591              
592             L, L, L.
593              
594             =cut