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   1135 use Mojo::Base -base;
  58         149  
  58         476  
3              
4 58     58   427 use Mojo::Asset::File;
  58         167  
  58         500  
5 58     58   350 use Mojo::Asset::Memory;
  58         128  
  58         466  
6 58     58   361 use Mojo::Content::MultiPart;
  58         132  
  58         560  
7 58     58   1059 use Mojo::Content::Single;
  58         443  
  58         658  
8 58     58   382 use Mojo::File qw(path);
  58         313  
  58         4245  
9 58     58   378 use Mojo::JSON qw(encode_json);
  58         138  
  58         3330  
10 58     58   397 use Mojo::Parameters;
  58         163  
  58         546  
11 58     58   33364 use Mojo::Transaction::HTTP;
  58         194  
  58         475  
12 58     58   470 use Mojo::Transaction::WebSocket;
  58         172  
  58         427  
13 58     58   369 use Mojo::URL;
  58         138  
  58         900  
14 58     58   338 use Mojo::Util qw(encode url_escape);
  58         153  
  58         4096  
15 58     58   390 use Mojo::WebSocket qw(challenge client_handshake);
  58         292  
  58         279650  
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 22 sub add_generator { $_[0]->generators->{$_[1]} = $_[2] and return $_[0] }
22              
23             sub download {
24 22     22 1 60 my ($self, $head, $path) = @_;
25              
26 22         53 my $req = $head->req;
27 22         58 my $tx = $self->tx(GET => $req->url->clone => $req->headers->to_hash);
28 22         86 my $res = $tx->res;
29 22 50       64 if (my $error = $head->error) { $res->error($error) and return $tx }
  2 100       12  
30              
31 20         38 my $headers = $head->res->headers;
32 20   100     50 my $accept_ranges = ($headers->accept_ranges // '') =~ /bytes/;
33 20   100     60 my $size = $headers->content_length // 0;
34              
35 20         64 my $current_size = 0;
36 20         73 my $file = path($path);
37 20 100       86 if (-f $file) {
38 12         31 $current_size = -s $file;
39 12 100 50     47 $res->error({message => 'Unknown file size'}) and return $tx unless $size;
40 10 100 50     39 $res->error({message => 'File size mismatch'}) and return $tx if $current_size > $size;
41 8 100 50     25 $res->error({message => 'Download complete'}) and return $tx if $current_size == $size;
42 6 100 50     19 $res->error({message => 'Server does not support partial requests'}) and return $tx unless $accept_ranges;
43 4         13 $tx->req->headers->range("bytes=$current_size-$size");
44             }
45              
46 12         55 my $fh = $file->open('>>');
47             $res->content->unsubscribe('read')->on(
48             read => sub {
49 10     10   66 my ($content, $bytes) = @_;
50 10         20 $current_size += length $bytes;
51 10 50       47 $fh->syswrite($bytes) == length $bytes or $res->error({message => qq/Can't write to file "$path": $!/});
52             }
53 12         51 );
54             $res->on(
55             finish => sub {
56 9     9   18 my $res = shift;
57 9 100       43 $res->error({message => 'Download incomplete'}) if $current_size < $size;
58             }
59 12         73 );
60              
61 12         63 return $tx;
62             }
63              
64             sub endpoint {
65 2190     2190 1 5451 my ($self, $tx) = @_;
66              
67             # Basic endpoint
68 2190         7064 my $req = $tx->req;
69 2190         6727 my $url = $req->url;
70 2190   50     9023 my $proto = $url->protocol || 'http';
71 2190         7472 my $host = $url->ihost;
72 2190 100 66     6531 my $port = $url->port // ($proto eq 'https' ? 443 : 80);
73              
74             # Proxy for normal HTTP requests
75 2190         4150 my $socks;
76 2190 100       6950 if (my $proxy = $req->proxy) { $socks = $proxy->protocol eq 'socks' }
  64         150  
77 2190 100 100     11175 return _proxy($tx, $proto, $host, $port) if $proto eq 'http' && !$req->is_handshake && !$socks;
      100        
78              
79 206         1165 return $proto, $host, $port;
80             }
81              
82 255     255 1 1361 sub peer { _proxy($_[1], $_[0]->endpoint($_[1])) }
83              
84             sub promisify {
85 47     47 1 137 my ($self, $promise, $tx) = @_;
86 47         185 my $err = $tx->error;
87 47 100 100     298 return $promise->reject($err->{message}) if $err && !$err->{code};
88 43 100 100     116 return $promise->reject('WebSocket handshake failed') if $tx->req->is_handshake && !$tx->is_websocket;
89 41         236 $promise->resolve($tx);
90             }
91              
92             sub proxy_connect {
93 232     232 1 1214 my ($self, $old) = @_;
94              
95             # Already a CONNECT request
96 232         714 my $req = $old->req;
97 232 100       1337 return undef if uc $req->method eq 'CONNECT';
98              
99             # No proxy
100 228 100 100     1165 return undef unless (my $proxy = $req->proxy) && $req->via_proxy;
101 7 100       29 return undef if $proxy->protocol eq 'socks';
102              
103             # WebSocket and/or HTTPS
104 5         18 my $url = $req->url;
105 5 100 100     19 return undef unless $req->is_handshake || $url->protocol eq 'https';
106              
107             # CONNECT request (expect a bad response)
108 3         19 my $new = $self->tx(CONNECT => $url->clone->userinfo(undef));
109 3         18 $new->req->proxy($proxy);
110 3         12 $new->res->content->auto_relax(0)->headers->connection('keep-alive');
111              
112 3         37 return $new;
113             }
114              
115             sub redirect {
116 971     971 1 2479 my ($self, $old) = @_;
117              
118             # Commonly used codes
119 971         2970 my $res = $old->res;
120 971   100     2688 my $code = $res->code // 0;
121 971 100       2462 return undef unless grep { $_ == $code } 301, 302, 303, 307, 308;
  4855         15433  
122              
123             # CONNECT requests cannot be redirected
124 58         215 my $req = $old->req;
125 58 100       231 return undef if uc $req->method eq 'CONNECT';
126              
127             # Fix location without authority and/or scheme
128 57 50       235 return undef unless my $location = $res->headers->every_header('Location')->[0];
129 57         316 $location = Mojo::URL->new($location);
130 57 100       235 $location = $location->base($req->url)->to_abs unless $location->is_abs;
131 57         246 my $proto = $location->protocol;
132 57 100 100     421 return undef if ($proto ne 'http' && $proto ne 'https') || !$location->host;
      100        
133              
134             # Clone request if necessary
135 55         231 my $new = Mojo::Transaction::HTTP->new;
136 55 100 100     360 if ($code == 307 || $code == 308) {
137 10 100       74 return undef unless my $clone = $req->clone;
138 8         39 $new->req($clone);
139             }
140             else {
141 45         191 my $method = uc $req->method;
142 45 100 100     256 $method = $code == 303 || $method eq 'POST' ? 'GET' : $method;
143 45         216 $new->req->method($method)->content->headers(my $headers = $req->headers->clone);
144 45         99 $headers->remove($_) for grep {/^content-/i} @{$headers->names};
  153         406  
  45         189  
145             }
146              
147 53         261 my $content = $new->res->content;
148 53 100       239 $content->auto_decompress(0) unless $self->compressed;
149 53         208 my $headers = $new->req->url($location)->headers;
150 53         297 $headers->remove($_) for qw(Authorization Cookie Host Referer);
151 53 100       177 if ($res->content->has_subscribers('sse')) { $content->on(sse => $_) for @{$res->content->subscribers('sse')} }
  1         2  
  1         4  
152              
153 53         266 return $new->previous($old);
154             }
155              
156             sub tx {
157 1128     1128 1 346586 my ($self, $method, $url) = (shift, shift, shift);
158              
159             # Method and URL
160 1128         8065 my $tx = Mojo::Transaction::HTTP->new;
161 1128         4826 my $req = $tx->req->method($method);
162 1128 100       5681 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         6071 my $headers = $req->headers;
166 1128 100       5101 $headers->from_hash(shift) if ref $_[0] eq 'HASH';
167 1128 100       4923 $headers->user_agent($self->name) unless $headers->user_agent;
168 1128 100       5019 if (!$self->compressed) { $tx->res->content->auto_decompress(0) }
  3 100       12  
169 1103         3176 elsif (!$headers->accept_encoding) { $headers->accept_encoding('gzip') }
170              
171             # Generator
172 1128 100       5103 if (@_ > 1) {
    100          
173 83         379 my $cb = $self->generators->{shift()};
174 83         332 $self->$cb($tx, @_);
175             }
176              
177             # Body
178 25         183 elsif (@_) { $req->body(shift) }
179              
180 1128         6568 return $tx;
181             }
182              
183             sub upgrade {
184 1022     1022 1 2810 my ($self, $tx) = @_;
185 1022   100     3152 my $code = $tx->res->code // 0;
186 1022 100 100     3319 return undef unless $tx->req->is_handshake && $code == 101;
187 75         404 my $ws = Mojo::Transaction::WebSocket->new(handshake => $tx, masked => 1);
188 75 50       367 return challenge($ws) ? $ws->established(1) : undef;
189             }
190              
191             sub websocket {
192 93     93 1 42325 my $self = shift;
193              
194             # New WebSocket transaction
195 93 100       439 my $sub = ref $_[-1] eq 'ARRAY' ? pop : [];
196 93         456 my $tx = $self->tx(GET => @_);
197 93         347 my $req = $tx->req;
198 93 100       350 $req->headers->sec_websocket_protocol(join ', ', @$sub) if @$sub;
199              
200             # Handshake protocol
201 93         306 my $url = $req->url;
202 93   50     454 my $proto = $url->protocol // '';
203 93 100       550 if ($proto eq 'ws') { $url->scheme('http') }
  7 100       20  
    100          
204 5         14 elsif ($proto eq 'wss') { $url->scheme('https') }
205 1         5 elsif ($proto eq 'ws+unix') { $url->scheme('http+unix') }
206              
207 93         441 return client_handshake $tx;
208             }
209              
210 31     31   317 sub _content { Mojo::Content::MultiPart->new(headers => $_[0], parts => $_[1]) }
211              
212             sub _form {
213 66     66   233 my ($self, $tx, $form, %options) = @_;
214 66 100       339 $options{charset} = 'UTF-8' unless exists $options{charset};
215              
216             # Check for uploads and force multipart if necessary
217 66         265 my $req = $tx->req;
218 66         253 my $headers = $req->headers;
219 66   100     270 my $multipart = ($headers->content_type // '') =~ m!multipart/form-data!i;
220 66 100       282 for my $value (map { ref $_ eq 'ARRAY' ? @$_ : $_ } values %$form) {
  104         493  
221 106 100 50     505 ++$multipart and last if ref $value eq 'HASH';
222             }
223              
224             # Multipart
225 66 100       226 if ($multipart) {
226 27         136 $req->content(_content($headers, _form_parts($options{charset}, $form)));
227 27         96 _type($headers, 'multipart/form-data');
228 27         95 return $tx;
229             }
230              
231             # Query parameters or urlencoded
232 39         164 my $method = uc $req->method;
233 39         200 my @form = map { $_ => $form->{$_} } sort keys %$form;
  63         224  
234 39 100 100     284 if ($method eq 'GET' || $method eq 'HEAD') { $req->url->query->merge(@form) }
  7         33  
235             else {
236 32         233 $req->body(Mojo::Parameters->new(@form)->charset($options{charset})->to_string);
237 32         252 _type($headers, 'application/x-www-form-urlencoded');
238             }
239              
240 39         175 return $tx;
241             }
242              
243             sub _form_parts {
244 27     27   77 my ($charset, $form) = @_;
245              
246 27         52 my @parts;
247 27         108 for my $name (sort keys %$form) {
248 41 100       145 next unless defined(my $values = $form->{$name});
249 40 100       146 $values = [$values] unless ref $values eq 'ARRAY';
250 40         76 push @parts, @{_parts($charset, $name, $values)};
  40         105  
251             }
252              
253 27         115 return \@parts;
254             }
255              
256             sub _json {
257 11     11   39 my ($self, $tx, $data) = @_;
258 11         44 _type($tx->req->body(encode_json $data)->headers, 'application/json');
259 11         31 return $tx;
260             }
261              
262             sub _multipart {
263 4     4   8 my ($self, $tx, $parts) = @_;
264 4         10 my $req = $tx->req;
265 4         10 $req->content(_content($req->headers, _parts(undef, undef, $parts)));
266 4         24 return $tx;
267             }
268              
269             sub _parts {
270 44     44   109 my ($charset, $name, $values) = @_;
271              
272 44         69 my @parts;
273 44         120 for my $value (@$values) {
274 55         214 push @parts, my $part = Mojo::Content::Single->new;
275              
276 55         76 my $filename;
277 55         161 my $headers = $part->headers;
278 55 100       192 if (ref $value eq 'HASH') {
279              
280             # File
281 30 100       134 if (my $file = delete $value->{file}) {
    50          
282 9 100       61 $file = Mojo::Asset::File->new(path => $file) unless ref $file;
283 9         35 $part->asset($file);
284 9 100 66     129 $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         105 $part->asset(Mojo::Asset::Memory->new->add_chunk($content));
290             }
291              
292             # Filename and headers
293 30         112 $filename = delete $value->{filename};
294 30         134 $headers->from_hash($value);
295 30 100       77 next unless defined $name;
296 26   66     161 $filename = url_escape $filename // $name, '"';
297 26 50       158 $filename = encode $charset, $filename if $charset;
298             }
299              
300             # Field
301             else {
302 25 100       132 $value = encode $charset, $value if $charset;
303 25         112 $part->asset(Mojo::Asset::Memory->new->add_chunk($value));
304             }
305              
306             # Content-Disposition
307 51 100 100     330 next if !defined $name || defined $headers->content_disposition;
308 48         127 $name = url_escape $name, '"';
309 48 100       210 $name = encode $charset, $name if $charset;
310 48         104 my $disposition = qq{form-data; name="$name"};
311 48 100       129 $disposition .= qq{; filename="$filename"} if defined $filename;
312 48         129 $headers->content_disposition($disposition);
313             }
314              
315 44         210 return \@parts;
316             }
317              
318             sub _proxy {
319 2239     2239   7435 my ($tx, $proto, $host, $port) = @_;
320              
321 2239         6161 my $req = $tx->req;
322 2239 100 100     7024 if ($req->via_proxy && (my $proxy = $req->proxy)) {
323 51 100 66     120 return $proxy->protocol, $proxy->ihost, $proxy->port // ($proto eq 'https' ? 443 : 80);
324             }
325              
326 2188         16912 return $proto, $host, $port;
327             }
328              
329 70 100   70   311 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