File Coverage

blib/lib/Mojo/UserAgent/Transactor.pm
Criterion Covered Total %
statement 225 225 100.0
branch 123 130 94.6
condition 73 84 86.9
subroutine 33 33 100.0
pod 10 10 100.0
total 464 482 96.2


line stmt bran cond sub pod time code
1             package Mojo::UserAgent::Transactor;
2 58     58   935 use Mojo::Base -base;
  58         293  
  58         369  
3              
4 58     58   316 use Mojo::Asset::File;
  58         123  
  58         407  
5 58     58   251 use Mojo::Asset::Memory;
  58         126  
  58         334  
6 58     58   223 use Mojo::Content::MultiPart;
  58         107  
  58         418  
7 58     58   249 use Mojo::Content::Single;
  58         97  
  58         298  
8 58     58   219 use Mojo::File qw(path);
  58         92  
  58         2886  
9 58     58   275 use Mojo::JSON qw(encode_json);
  58         108  
  58         2086  
10 58     58   248 use Mojo::Parameters;
  58         295  
  58         347  
11 58     58   23639 use Mojo::Transaction::HTTP;
  58         155  
  58         369  
12 58     58   319 use Mojo::Transaction::WebSocket;
  58         249  
  58         283  
13 58     58   205 use Mojo::URL;
  58         1953  
  58         2202  
14 58     58   1004 use Mojo::Util qw(encode url_escape);
  58         373  
  58         3041  
15 58     58   415 use Mojo::WebSocket qw(challenge client_handshake);
  58         79  
  58         184984  
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 53 my ($self, $head, $path) = @_;
25              
26 22         41 my $req = $head->req;
27 22         49 my $tx = $self->tx(GET => $req->url->clone => $req->headers->to_hash);
28 22         87 my $res = $tx->res;
29 22 50       62 if (my $error = $head->error) { $res->error($error) and return $tx }
  2 100       8  
30              
31 20         52 my $headers = $head->res->headers;
32 20   100     46 my $accept_ranges = ($headers->accept_ranges // '') =~ /bytes/;
33 20   100     43 my $size = $headers->content_length // 0;
34              
35 20         31 my $current_size = 0;
36 20         75 my $file = path($path);
37 20 100       80 if (-f $file) {
38 12         39 $current_size = -s $file;
39 12 100 50     56 $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     28 $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         14 $tx->req->headers->range("bytes=$current_size-$size");
44             }
45              
46 12         54 my $fh = $file->open('>>');
47             $res->content->unsubscribe('read')->on(
48             read => sub {
49 10     10   17 my ($content, $bytes) = @_;
50 10         14 $current_size += length $bytes;
51 10 50       43 $fh->syswrite($bytes) == length $bytes or $res->error({message => qq/Can't write to file "$path": $!/});
52             }
53 12         61 );
54             $res->on(
55             finish => sub {
56 9     9   12 my $res = shift;
57 9 100       48 $res->error({message => 'Download incomplete'}) if $current_size < $size;
58             }
59 12         71 );
60              
61 12         77 return $tx;
62             }
63              
64             sub endpoint {
65 2198     2198 1 3737 my ($self, $tx) = @_;
66              
67             # Basic endpoint
68 2198         4375 my $req = $tx->req;
69 2198         4601 my $url = $req->url;
70 2198   50     6138 my $proto = $url->protocol || 'http';
71 2198         4795 my $host = $url->ihost;
72 2198 100 66     4573 my $port = $url->port // ($proto eq 'https' ? 443 : 80);
73              
74             # Proxy for normal HTTP requests
75 2198         2764 my $socks;
76 2198 100       5016 if (my $proxy = $req->proxy) { $socks = $proxy->protocol eq 'socks' }
  64         92  
77 2198 100 100     7476 return _proxy($tx, $proto, $host, $port) if $proto eq 'http' && !$req->is_handshake && !$socks;
      100        
78              
79 206         879 return $proto, $host, $port;
80             }
81              
82 255     255 1 803 sub peer { _proxy($_[1], $_[0]->endpoint($_[1])) }
83              
84             sub promisify {
85 48     48 1 107 my ($self, $promise, $tx) = @_;
86 48         142 my $err = $tx->error;
87 48 100 100     158 return $promise->reject($err->{message}) if $err && !$err->{code};
88 44 100 100     120 return $promise->reject('WebSocket handshake failed') if $tx->req->is_handshake && !$tx->is_websocket;
89 42         191 $promise->resolve($tx);
90             }
91              
92             sub proxy_connect {
93 232     232 1 509 my ($self, $old) = @_;
94              
95             # Already a CONNECT request
96 232         485 my $req = $old->req;
97 232 100       537 return undef if uc $req->method eq 'CONNECT';
98              
99             # No proxy
100 228 100 100     602 return undef unless (my $proxy = $req->proxy) && $req->via_proxy;
101 7 100       18 return undef if $proxy->protocol eq 'socks';
102              
103             # WebSocket and/or HTTPS
104 5         11 my $url = $req->url;
105 5 100 100     15 return undef unless $req->is_handshake || $url->protocol eq 'https';
106              
107             # CONNECT request (expect a bad response)
108 3         11 my $new = $self->tx(CONNECT => $url->clone->userinfo(undef));
109 3         8 $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 979     979 1 1744 my ($self, $old) = @_;
117              
118             # Commonly used codes
119 979         1976 my $res = $old->res;
120 979   100     1826 my $code = $res->code // 0;
121 979 100       1718 return undef unless grep { $_ == $code } 301, 302, 303, 307, 308;
  4895         10420  
122              
123             # CONNECT requests cannot be redirected
124 62         144 my $req = $old->req;
125 62 100       189 return undef if uc $req->method eq 'CONNECT';
126              
127             # Fix location without authority and/or scheme
128 61 50       231 return undef unless my $location = $res->headers->every_header('Location')->[0];
129 61         237 $location = Mojo::URL->new($location);
130 61 100       172 $location = $location->base($req->url)->to_abs unless $location->is_abs;
131 61         187 my $proto = $location->protocol;
132 61 100 100     2976 return undef if ($proto ne 'http' && $proto ne 'https') || !$location->host;
      100        
133              
134             # Clone request if necessary (QUERY is safe and idempotent, so it keeps its content like 307 and 308)
135 59         208 my $new = Mojo::Transaction::HTTP->new;
136 59         123 my $method = uc $req->method;
137 59 100 100     375 if ($code == 307 || $code == 308 || ($method eq 'QUERY' && $code != 303)) {
      100        
      100        
138 13 100       45 return undef unless my $clone = $req->clone;
139 11         27 $new->req($clone);
140             }
141             else {
142 46 100 100     219 $method = $code == 303 || $method eq 'POST' ? 'GET' : $method;
143 46         111 $new->req->method($method)->content->headers(my $headers = $req->headers->clone);
144 46         77 $headers->remove($_) for grep {/^content-/i} @{$headers->names};
  156         325  
  46         123  
145             }
146              
147 57         147 my $content = $new->res->content;
148 57 100       179 $content->auto_decompress(0) unless $self->compressed;
149 57         155 my $headers = $new->req->url($location)->headers;
150 57         218 $headers->remove($_) for qw(Authorization Cookie Host Referer);
151 57 100       119 if ($res->content->has_subscribers('sse')) { $content->on(sse => $_) for @{$res->content->subscribers('sse')} }
  1         2  
  1         2  
152              
153 57         178 return $new->previous($old);
154             }
155              
156             sub tx {
157 1136     1136 1 250012 my ($self, $method, $url) = (shift, shift, shift);
158              
159             # Method and URL
160 1136         5690 my $tx = Mojo::Transaction::HTTP->new;
161 1136         3494 my $req = $tx->req->method($method);
162 1136 100       3986 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 1136         3937 my $headers = $req->headers;
166 1136 100       3800 $headers->from_hash(shift) if ref $_[0] eq 'HASH';
167 1136 100       3591 $headers->user_agent($self->name) unless $headers->user_agent;
168 1136 100       3522 if (!$self->compressed) { $tx->res->content->auto_decompress(0) }
  3 100       9  
169 1111         2302 elsif (!$headers->accept_encoding) { $headers->accept_encoding('gzip') }
170              
171             # Generator
172 1136 100       3501 if (@_ > 1) {
    100          
173 83         291 my $cb = $self->generators->{shift()};
174 83         250 $self->$cb($tx, @_);
175             }
176              
177             # Body
178 29         143 elsif (@_) { $req->body(shift) }
179              
180 1136         5130 return $tx;
181             }
182              
183             sub upgrade {
184 1026     1026 1 1906 my ($self, $tx) = @_;
185 1026   100     2121 my $code = $tx->res->code // 0;
186 1026 100 100     2221 return undef unless $tx->req->is_handshake && $code == 101;
187 75         321 my $ws = Mojo::Transaction::WebSocket->new(handshake => $tx, masked => 1);
188 75 50       356 return challenge($ws) ? $ws->established(1) : undef;
189             }
190              
191             sub websocket {
192 93     93 1 25832 my $self = shift;
193              
194             # New WebSocket transaction
195 93 100       366 my $sub = ref $_[-1] eq 'ARRAY' ? pop : [];
196 93         372 my $tx = $self->tx(GET => @_);
197 93         282 my $req = $tx->req;
198 93 100       334 $req->headers->sec_websocket_protocol(join ', ', @$sub) if @$sub;
199              
200             # Handshake protocol
201 93         294 my $url = $req->url;
202 93   50     303 my $proto = $url->protocol // '';
203 93 100       507 if ($proto eq 'ws') { $url->scheme('http') }
  7 100       13  
    100          
204 5         24 elsif ($proto eq 'wss') { $url->scheme('https') }
205 1         4 elsif ($proto eq 'ws+unix') { $url->scheme('http+unix') }
206              
207 93         372 return client_handshake $tx;
208             }
209              
210 31     31   211 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       232 $options{charset} = 'UTF-8' unless exists $options{charset};
215              
216             # Check for uploads and force multipart if necessary
217 66         165 my $req = $tx->req;
218 66         161 my $headers = $req->headers;
219 66   100     204 my $multipart = ($headers->content_type // '') =~ m!multipart/form-data!i;
220 66 100       189 for my $value (map { ref $_ eq 'ARRAY' ? @$_ : $_ } values %$form) {
  104         347  
221 113 100 50     293 ++$multipart and last if ref $value eq 'HASH';
222             }
223              
224             # Multipart
225 66 100       225 if ($multipart) {
226 27         86 $req->content(_content($headers, _form_parts($options{charset}, $form)));
227 27         78 _type($headers, 'multipart/form-data');
228 27         69 return $tx;
229             }
230              
231             # Query parameters or urlencoded
232 39         96 my $method = uc $req->method;
233 39         118 my @form = map { $_ => $form->{$_} } sort keys %$form;
  63         155  
234 39 100 100     176 if ($method eq 'GET' || $method eq 'HEAD') { $req->url->query->merge(@form) }
  7         19  
235             else {
236 32         142 $req->body(Mojo::Parameters->new(@form)->charset($options{charset})->to_string);
237 32         157 _type($headers, 'application/x-www-form-urlencoded');
238             }
239              
240 39         117 return $tx;
241             }
242              
243             sub _form_parts {
244 27     27   61 my ($charset, $form) = @_;
245              
246 27         56 my @parts;
247 27         96 for my $name (sort keys %$form) {
248 41 100       110 next unless defined(my $values = $form->{$name});
249 40 100       107 $values = [$values] unless ref $values eq 'ARRAY';
250 40         58 push @parts, @{_parts($charset, $name, $values)};
  40         94  
251             }
252              
253 27         85 return \@parts;
254             }
255              
256             sub _json {
257 11     11   27 my ($self, $tx, $data) = @_;
258 11         35 _type($tx->req->body(encode_json $data)->headers, 'application/json');
259 11         24 return $tx;
260             }
261              
262             sub _multipart {
263 4     4   7 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   105 my ($charset, $name, $values) = @_;
271              
272 44         76 my @parts;
273 44         77 for my $value (@$values) {
274 55         131 push @parts, my $part = Mojo::Content::Single->new;
275              
276 55         64 my $filename;
277 55         143 my $headers = $part->headers;
278 55 100       149 if (ref $value eq 'HASH') {
279              
280             # File
281 30 100       127 if (my $file = delete $value->{file}) {
    50          
282 9 100       54 $file = Mojo::Asset::File->new(path => $file) unless ref $file;
283 9         27 $part->asset($file);
284 9 100 66     108 $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         80 $part->asset(Mojo::Asset::Memory->new->add_chunk($content));
290             }
291              
292             # Filename and headers
293 30         66 $filename = delete $value->{filename};
294 30         100 $headers->from_hash($value);
295 30 100       72 next unless defined $name;
296 26   66     115 $filename = url_escape $filename // $name, '"';
297 26 50       103 $filename = encode $charset, $filename if $charset;
298             }
299              
300             # Field
301             else {
302 25 100       72 $value = encode $charset, $value if $charset;
303 25         71 $part->asset(Mojo::Asset::Memory->new->add_chunk($value));
304             }
305              
306             # Content-Disposition
307 51 100 100     237 next if !defined $name || defined $headers->content_disposition;
308 48         99 $name = url_escape $name, '"';
309 48 100       120 $name = encode $charset, $name if $charset;
310 48         84 my $disposition = qq{form-data; name="$name"};
311 48 100       100 $disposition .= qq{; filename="$filename"} if defined $filename;
312 48         90 $headers->content_disposition($disposition);
313             }
314              
315 44         136 return \@parts;
316             }
317              
318             sub _proxy {
319 2247     2247   5212 my ($tx, $proto, $host, $port) = @_;
320              
321 2247         4236 my $req = $tx->req;
322 2247 100 100     4547 if ($req->via_proxy && (my $proxy = $req->proxy)) {
323 51 100 66     81 return $proxy->protocol, $proxy->ihost, $proxy->port // ($proto eq 'https' ? 443 : 80);
324             }
325              
326 2196         10904 return $proto, $host, $port;
327             }
328              
329 70 100   70   194 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. Since C requests are safe and idempotent, they are never redirected as C requests and keep their
454             content, except for C<303> responses.
455              
456             =head2 tx
457              
458             my $tx = $t->tx(GET => 'example.com');
459             my $tx = $t->tx(POST => 'http://example.com');
460             my $tx = $t->tx(GET => 'http://example.com' => {Accept => '*/*'});
461             my $tx = $t->tx(PUT => 'http://example.com' => 'Content!');
462             my $tx = $t->tx(PUT => 'http://example.com' => form => {a => 'b'});
463             my $tx = $t->tx(PUT => 'http://example.com' => json => {a => 'b'});
464             my $tx = $t->tx(PUT => 'https://example.com' => multipart => ['a', 'b']);
465             my $tx = $t->tx(POST => 'example.com' => {Accept => '*/*'} => 'Content!');
466             my $tx = $t->tx(PUT => 'example.com' => {Accept => '*/*'} => form => {a => 'b'});
467             my $tx = $t->tx(PUT => 'example.com' => {Accept => '*/*'} => json => {a => 'b'});
468             my $tx = $t->tx(PUT => 'example.com' => {Accept => '*/*'} => multipart => ['a', 'b']);
469              
470             Versatile general purpose L transaction builder for requests, with support for
471             L.
472              
473             # Generate and inspect custom GET request with DNT header and content
474             say $t->tx(GET => 'example.com' => {DNT => 1} => 'Bye!')->req->to_string;
475              
476             # Stream response content to STDOUT
477             my $tx = $t->tx(GET => 'http://example.com');
478             $tx->res->content->unsubscribe('read')->on(read => sub { say $_[1] });
479              
480             # PUT request with content streamed from file
481             my $tx = $t->tx(PUT => 'http://example.com');
482             $tx->req->content->asset(Mojo::Asset::File->new(path => '/foo.txt'));
483              
484             The C content generator uses L for encoding and sets the content type to C.
485              
486             # POST request with "application/json" content
487             my $tx = $t->tx(POST => 'http://example.com' => json => {a => 'b', c => [1, 2, 3]});
488              
489             The C content generator will automatically use query parameters for C and C requests.
490              
491             # GET request with query parameters
492             my $tx = $t->tx(GET => 'http://example.com' => form => {a => 'b'});
493              
494             For all other request methods the C content type is used.
495              
496             # POST request with "application/x-www-form-urlencoded" content
497             my $tx = $t->tx(POST => 'http://example.com' => form => {a => 'b', c => 'd'});
498              
499             Parameters may be encoded with the C option.
500              
501             # PUT request with Shift_JIS encoded form values
502             my $tx = $t->tx(PUT => 'example.com' => form => {a => 'b'} => charset => 'Shift_JIS');
503              
504             An array reference can be used for multiple form values sharing the same name.
505              
506             # POST request with form values sharing the same name
507             my $tx = $t->tx(POST => 'http://example.com' => form => {a => ['b', 'c', 'd']});
508              
509             A hash reference with a C or C value can be used to switch to the C content type
510             for file uploads.
511              
512             # POST request with "multipart/form-data" content
513             my $tx = $t->tx(POST => 'http://example.com' => form => {mytext => {content => 'lala'}});
514              
515             # POST request with multiple files sharing the same name
516             my $tx = $t->tx(POST => 'http://example.com' => form => {mytext => [{content => 'first'}, {content => 'second'}]});
517              
518             The C value should contain the path to the file you want to upload or an asset object, like L
519             or L.
520              
521             # POST request with upload streamed from file
522             my $tx = $t->tx(POST => 'http://example.com' => form => {mytext => {file => '/foo.txt'}});
523              
524             # POST request with upload streamed from asset
525             my $asset = Mojo::Asset::Memory->new->add_chunk('lalala');
526             my $tx = $t->tx(POST => 'http://example.com' => form => {mytext => {file => $asset}});
527              
528             A C value will be generated automatically, but can also be set manually if necessary. All remaining values in
529             the hash reference get merged into the C content as headers.
530              
531             # POST request with form values and customized upload (filename and header)
532             my $tx = $t->tx(POST => 'http://example.com' => form => {
533             a => 'b',
534             c => 'd',
535             mytext => {
536             content => 'lalala',
537             filename => 'foo.txt',
538             'Content-Type' => 'text/plain'
539             }
540             });
541              
542             The C content type can also be enforced by setting the C header manually.
543              
544             # Force "multipart/form-data"
545             my $headers = {'Content-Type' => 'multipart/form-data'};
546             my $tx = $t->tx(POST => 'example.com' => $headers => form => {a => 'b'});
547              
548             The C content generator can be used to build custom multipart requests and does not set a content type.
549              
550             # POST request with multipart content ("foo" and "bar")
551             my $tx = $t->tx(POST => 'http://example.com' => multipart => ['foo', 'bar']);
552              
553             Similar to the C content generator you can also pass hash references with C or C values, as well
554             as headers.
555              
556             # POST request with multipart content streamed from file
557             my $tx = $t->tx(POST => 'http://example.com' => multipart => [{file => '/foo.txt'}]);
558              
559             # PUT request with multipart content streamed from asset
560             my $headers = {'Content-Type' => 'multipart/custom'};
561             my $asset = Mojo::Asset::Memory->new->add_chunk('lalala');
562             my $tx = $t->tx(PUT => 'http://example.com' => $headers => multipart => [{file => $asset}]);
563              
564             # POST request with multipart content and custom headers
565             my $tx = $t->tx(POST => 'http://example.com' => multipart => [
566             {
567             content => 'Hello',
568             'Content-Type' => 'text/plain',
569             'Content-Language' => 'en-US'
570             },
571             {
572             content => 'World!',
573             'Content-Type' => 'text/plain',
574             'Content-Language' => 'en-US'
575             }
576             ]);
577              
578             =head2 upgrade
579              
580             my $tx = $t->upgrade(Mojo::Transaction::HTTP->new);
581              
582             Build L follow-up transaction for WebSocket handshake if possible.
583              
584             =head2 websocket
585              
586             my $tx = $t->websocket('ws://example.com');
587             my $tx = $t->websocket('ws://example.com' => {DNT => 1} => ['v1.proto']);
588              
589             Versatile L transaction builder for WebSocket handshake requests.
590              
591             =head1 SEE ALSO
592              
593             L, L, L.
594              
595             =cut