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