puma 4.3.12 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of puma might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/History.md +1591 -521
- data/LICENSE +23 -20
- data/README.md +130 -42
- data/bin/puma-wild +3 -9
- data/docs/architecture.md +63 -26
- data/docs/compile_options.md +55 -0
- data/docs/deployment.md +60 -69
- data/docs/fork_worker.md +31 -0
- data/docs/jungle/README.md +9 -0
- data/{tools → docs}/jungle/rc.d/README.md +1 -1
- data/{tools → docs}/jungle/rc.d/puma +2 -2
- data/{tools → docs}/jungle/rc.d/puma.conf +0 -0
- data/docs/kubernetes.md +66 -0
- data/docs/nginx.md +1 -1
- data/docs/plugins.md +15 -15
- data/docs/rails_dev_mode.md +28 -0
- data/docs/restart.md +46 -23
- data/docs/signals.md +13 -11
- data/docs/stats.md +142 -0
- data/docs/systemd.md +85 -128
- data/docs/testing_benchmarks_local_files.md +150 -0
- data/docs/testing_test_rackup_ci_files.md +36 -0
- data/ext/puma_http11/PumaHttp11Service.java +2 -4
- data/ext/puma_http11/ext_help.h +1 -1
- data/ext/puma_http11/extconf.rb +49 -12
- data/ext/puma_http11/http11_parser.c +46 -48
- data/ext/puma_http11/http11_parser.h +2 -2
- data/ext/puma_http11/http11_parser.java.rl +3 -3
- data/ext/puma_http11/http11_parser.rl +3 -3
- data/ext/puma_http11/http11_parser_common.rl +2 -2
- data/ext/puma_http11/mini_ssl.c +250 -93
- data/ext/puma_http11/no_ssl/PumaHttp11Service.java +15 -0
- data/ext/puma_http11/org/jruby/puma/Http11.java +6 -6
- data/ext/puma_http11/org/jruby/puma/Http11Parser.java +4 -6
- data/ext/puma_http11/org/jruby/puma/MiniSSL.java +241 -96
- data/ext/puma_http11/puma_http11.c +46 -57
- data/lib/puma/app/status.rb +52 -38
- data/lib/puma/binder.rb +232 -119
- data/lib/puma/cli.rb +33 -33
- data/lib/puma/client.rb +125 -87
- data/lib/puma/cluster/worker.rb +175 -0
- data/lib/puma/cluster/worker_handle.rb +97 -0
- data/lib/puma/cluster.rb +224 -229
- data/lib/puma/commonlogger.rb +2 -2
- data/lib/puma/configuration.rb +112 -87
- data/lib/puma/const.rb +25 -22
- data/lib/puma/control_cli.rb +99 -79
- data/lib/puma/detect.rb +31 -2
- data/lib/puma/dsl.rb +423 -110
- data/lib/puma/error_logger.rb +112 -0
- data/lib/puma/events.rb +16 -115
- data/lib/puma/io_buffer.rb +34 -2
- data/lib/puma/jruby_restart.rb +2 -59
- data/lib/puma/json_serialization.rb +96 -0
- data/lib/puma/launcher/bundle_pruner.rb +104 -0
- data/lib/puma/launcher.rb +170 -148
- data/lib/puma/log_writer.rb +137 -0
- data/lib/puma/minissl/context_builder.rb +35 -19
- data/lib/puma/minissl.rb +213 -55
- data/lib/puma/null_io.rb +18 -1
- data/lib/puma/plugin/tmp_restart.rb +1 -1
- data/lib/puma/plugin.rb +3 -12
- data/lib/puma/rack/builder.rb +5 -9
- data/lib/puma/rack_default.rb +1 -1
- data/lib/puma/reactor.rb +85 -369
- data/lib/puma/request.rb +607 -0
- data/lib/puma/runner.rb +83 -77
- data/lib/puma/server.rb +305 -789
- data/lib/puma/single.rb +18 -74
- data/lib/puma/state_file.rb +45 -8
- data/lib/puma/systemd.rb +47 -0
- data/lib/puma/thread_pool.rb +137 -66
- data/lib/puma/util.rb +21 -4
- data/lib/puma.rb +54 -5
- data/lib/rack/handler/puma.rb +11 -12
- data/tools/{docker/Dockerfile → Dockerfile} +1 -1
- metadata +31 -23
- data/docs/tcp_mode.md +0 -96
- data/ext/puma_http11/io_buffer.c +0 -155
- data/ext/puma_http11/org/jruby/puma/IOBuffer.java +0 -72
- data/lib/puma/accept_nonblock.rb +0 -29
- data/lib/puma/tcp_logger.rb +0 -41
- data/tools/jungle/README.md +0 -19
- data/tools/jungle/init.d/README.md +0 -61
- data/tools/jungle/init.d/puma +0 -421
- data/tools/jungle/init.d/run-puma +0 -18
- data/tools/jungle/upstart/README.md +0 -61
- data/tools/jungle/upstart/puma-manager.conf +0 -31
- data/tools/jungle/upstart/puma.conf +0 -69
@@ -36,11 +36,13 @@ static VALUE global_request_method;
|
|
36
36
|
static VALUE global_request_uri;
|
37
37
|
static VALUE global_fragment;
|
38
38
|
static VALUE global_query_string;
|
39
|
-
static VALUE
|
39
|
+
static VALUE global_server_protocol;
|
40
40
|
static VALUE global_request_path;
|
41
41
|
|
42
42
|
/** Defines common length and error messages for input length validation. */
|
43
|
-
#define
|
43
|
+
#define QUOTE(s) #s
|
44
|
+
#define EXPAND_MAX_LENGTH_VALUE(s) QUOTE(s)
|
45
|
+
#define DEF_MAX_LENGTH(N,length) const size_t MAX_##N##_LENGTH = length; const char *MAX_##N##_LENGTH_ERR = "HTTP element " # N " is longer than the " EXPAND_MAX_LENGTH_VALUE(length) " allowed length (was %d)"
|
44
46
|
|
45
47
|
/** Validates the max length of given input and throws an HttpParserError exception if over. */
|
46
48
|
#define VALIDATE_MAX_LENGTH(len, N) if(len > MAX_##N##_LENGTH) { rb_raise(eHttpParserError, MAX_##N##_LENGTH_ERR, len); }
|
@@ -50,12 +52,24 @@ static VALUE global_request_path;
|
|
50
52
|
|
51
53
|
|
52
54
|
/* Defines the maximum allowed lengths for various input elements.*/
|
55
|
+
#ifndef PUMA_REQUEST_URI_MAX_LENGTH
|
56
|
+
#define PUMA_REQUEST_URI_MAX_LENGTH (1024 * 12)
|
57
|
+
#endif
|
58
|
+
|
59
|
+
#ifndef PUMA_REQUEST_PATH_MAX_LENGTH
|
60
|
+
#define PUMA_REQUEST_PATH_MAX_LENGTH (8192)
|
61
|
+
#endif
|
62
|
+
|
63
|
+
#ifndef PUMA_QUERY_STRING_MAX_LENGTH
|
64
|
+
#define PUMA_QUERY_STRING_MAX_LENGTH (1024 * 10)
|
65
|
+
#endif
|
66
|
+
|
53
67
|
DEF_MAX_LENGTH(FIELD_NAME, 256);
|
54
68
|
DEF_MAX_LENGTH(FIELD_VALUE, 80 * 1024);
|
55
|
-
DEF_MAX_LENGTH(REQUEST_URI,
|
69
|
+
DEF_MAX_LENGTH(REQUEST_URI, PUMA_REQUEST_URI_MAX_LENGTH);
|
56
70
|
DEF_MAX_LENGTH(FRAGMENT, 1024); /* Don't know if this length is specified somewhere or not */
|
57
|
-
DEF_MAX_LENGTH(REQUEST_PATH,
|
58
|
-
DEF_MAX_LENGTH(QUERY_STRING,
|
71
|
+
DEF_MAX_LENGTH(REQUEST_PATH, PUMA_REQUEST_PATH_MAX_LENGTH);
|
72
|
+
DEF_MAX_LENGTH(QUERY_STRING, PUMA_QUERY_STRING_MAX_LENGTH);
|
59
73
|
DEF_MAX_LENGTH(HEADER, (1024 * (80 + 32)));
|
60
74
|
|
61
75
|
struct common_field {
|
@@ -112,21 +126,6 @@ static struct common_field common_http_fields[] = {
|
|
112
126
|
# undef f
|
113
127
|
};
|
114
128
|
|
115
|
-
/*
|
116
|
-
* qsort(3) and bsearch(3) improve average performance slightly, but may
|
117
|
-
* not be worth it for lack of portability to certain platforms...
|
118
|
-
*/
|
119
|
-
#if defined(HAVE_QSORT_BSEARCH)
|
120
|
-
/* sort by length, then by name if there's a tie */
|
121
|
-
static int common_field_cmp(const void *a, const void *b)
|
122
|
-
{
|
123
|
-
struct common_field *cfa = (struct common_field *)a;
|
124
|
-
struct common_field *cfb = (struct common_field *)b;
|
125
|
-
signed long diff = cfa->len - cfb->len;
|
126
|
-
return diff ? diff : memcmp(cfa->name, cfb->name, cfa->len);
|
127
|
-
}
|
128
|
-
#endif /* HAVE_QSORT_BSEARCH */
|
129
|
-
|
130
129
|
static void init_common_fields(void)
|
131
130
|
{
|
132
131
|
unsigned i;
|
@@ -143,28 +142,10 @@ static void init_common_fields(void)
|
|
143
142
|
}
|
144
143
|
rb_global_variable(&cf->value);
|
145
144
|
}
|
146
|
-
|
147
|
-
#if defined(HAVE_QSORT_BSEARCH)
|
148
|
-
qsort(common_http_fields,
|
149
|
-
ARRAY_SIZE(common_http_fields),
|
150
|
-
sizeof(struct common_field),
|
151
|
-
common_field_cmp);
|
152
|
-
#endif /* HAVE_QSORT_BSEARCH */
|
153
145
|
}
|
154
146
|
|
155
147
|
static VALUE find_common_field_value(const char *field, size_t flen)
|
156
148
|
{
|
157
|
-
#if defined(HAVE_QSORT_BSEARCH)
|
158
|
-
struct common_field key;
|
159
|
-
struct common_field *found;
|
160
|
-
key.name = field;
|
161
|
-
key.len = (signed long)flen;
|
162
|
-
found = (struct common_field *)bsearch(&key, common_http_fields,
|
163
|
-
ARRAY_SIZE(common_http_fields),
|
164
|
-
sizeof(struct common_field),
|
165
|
-
common_field_cmp);
|
166
|
-
return found ? found->value : Qnil;
|
167
|
-
#else /* !HAVE_QSORT_BSEARCH */
|
168
149
|
unsigned i;
|
169
150
|
struct common_field *cf = common_http_fields;
|
170
151
|
for(i = 0; i < ARRAY_SIZE(common_http_fields); i++, cf++) {
|
@@ -172,7 +153,6 @@ static VALUE find_common_field_value(const char *field, size_t flen)
|
|
172
153
|
return cf->value;
|
173
154
|
}
|
174
155
|
return Qnil;
|
175
|
-
#endif /* !HAVE_QSORT_BSEARCH */
|
176
156
|
}
|
177
157
|
|
178
158
|
void http_field(puma_parser* hp, const char *field, size_t flen,
|
@@ -264,10 +244,10 @@ void query_string(puma_parser* hp, const char *at, size_t length)
|
|
264
244
|
rb_hash_aset(hp->request, global_query_string, val);
|
265
245
|
}
|
266
246
|
|
267
|
-
void
|
247
|
+
void server_protocol(puma_parser* hp, const char *at, size_t length)
|
268
248
|
{
|
269
249
|
VALUE val = rb_str_new(at, length);
|
270
|
-
rb_hash_aset(hp->request,
|
250
|
+
rb_hash_aset(hp->request, global_server_protocol, val);
|
271
251
|
}
|
272
252
|
|
273
253
|
/** Finalizes the request header to have a bunch of stuff that's
|
@@ -287,11 +267,18 @@ void HttpParser_free(void *data) {
|
|
287
267
|
}
|
288
268
|
}
|
289
269
|
|
290
|
-
void HttpParser_mark(
|
270
|
+
void HttpParser_mark(void *ptr) {
|
271
|
+
puma_parser *hp = ptr;
|
291
272
|
if(hp->request) rb_gc_mark(hp->request);
|
292
273
|
if(hp->body) rb_gc_mark(hp->body);
|
293
274
|
}
|
294
275
|
|
276
|
+
const rb_data_type_t HttpParser_data_type = {
|
277
|
+
"HttpParser",
|
278
|
+
{ HttpParser_mark, HttpParser_free, 0 },
|
279
|
+
0, 0, RUBY_TYPED_FREE_IMMEDIATELY,
|
280
|
+
};
|
281
|
+
|
295
282
|
VALUE HttpParser_alloc(VALUE klass)
|
296
283
|
{
|
297
284
|
puma_parser *hp = ALLOC_N(puma_parser, 1);
|
@@ -302,13 +289,13 @@ VALUE HttpParser_alloc(VALUE klass)
|
|
302
289
|
hp->fragment = fragment;
|
303
290
|
hp->request_path = request_path;
|
304
291
|
hp->query_string = query_string;
|
305
|
-
hp->
|
292
|
+
hp->server_protocol = server_protocol;
|
306
293
|
hp->header_done = header_done;
|
307
294
|
hp->request = Qnil;
|
308
295
|
|
309
296
|
puma_parser_init(hp);
|
310
297
|
|
311
|
-
return
|
298
|
+
return TypedData_Wrap_Struct(klass, &HttpParser_data_type, hp);
|
312
299
|
}
|
313
300
|
|
314
301
|
/**
|
@@ -320,7 +307,7 @@ VALUE HttpParser_alloc(VALUE klass)
|
|
320
307
|
VALUE HttpParser_init(VALUE self)
|
321
308
|
{
|
322
309
|
puma_parser *http = NULL;
|
323
|
-
DATA_GET(self, puma_parser, http);
|
310
|
+
DATA_GET(self, puma_parser, &HttpParser_data_type, http);
|
324
311
|
puma_parser_init(http);
|
325
312
|
|
326
313
|
return self;
|
@@ -337,7 +324,7 @@ VALUE HttpParser_init(VALUE self)
|
|
337
324
|
VALUE HttpParser_reset(VALUE self)
|
338
325
|
{
|
339
326
|
puma_parser *http = NULL;
|
340
|
-
DATA_GET(self, puma_parser, http);
|
327
|
+
DATA_GET(self, puma_parser, &HttpParser_data_type, http);
|
341
328
|
puma_parser_init(http);
|
342
329
|
|
343
330
|
return Qnil;
|
@@ -354,7 +341,7 @@ VALUE HttpParser_reset(VALUE self)
|
|
354
341
|
VALUE HttpParser_finish(VALUE self)
|
355
342
|
{
|
356
343
|
puma_parser *http = NULL;
|
357
|
-
DATA_GET(self, puma_parser, http);
|
344
|
+
DATA_GET(self, puma_parser, &HttpParser_data_type, http);
|
358
345
|
puma_parser_finish(http);
|
359
346
|
|
360
347
|
return puma_parser_is_finished(http) ? Qtrue : Qfalse;
|
@@ -385,7 +372,7 @@ VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data, VALUE start)
|
|
385
372
|
char *dptr = NULL;
|
386
373
|
long dlen = 0;
|
387
374
|
|
388
|
-
DATA_GET(self, puma_parser, http);
|
375
|
+
DATA_GET(self, puma_parser, &HttpParser_data_type, http);
|
389
376
|
|
390
377
|
from = FIX2INT(start);
|
391
378
|
dptr = rb_extract_chars(data, &dlen);
|
@@ -401,7 +388,7 @@ VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data, VALUE start)
|
|
401
388
|
VALIDATE_MAX_LENGTH(puma_parser_nread(http), HEADER);
|
402
389
|
|
403
390
|
if(puma_parser_has_error(http)) {
|
404
|
-
rb_raise(eHttpParserError, "%s", "Invalid HTTP format, parsing fails.");
|
391
|
+
rb_raise(eHttpParserError, "%s", "Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma?");
|
405
392
|
} else {
|
406
393
|
return INT2FIX(puma_parser_nread(http));
|
407
394
|
}
|
@@ -419,7 +406,7 @@ VALUE HttpParser_execute(VALUE self, VALUE req_hash, VALUE data, VALUE start)
|
|
419
406
|
VALUE HttpParser_has_error(VALUE self)
|
420
407
|
{
|
421
408
|
puma_parser *http = NULL;
|
422
|
-
DATA_GET(self, puma_parser, http);
|
409
|
+
DATA_GET(self, puma_parser, &HttpParser_data_type, http);
|
423
410
|
|
424
411
|
return puma_parser_has_error(http) ? Qtrue : Qfalse;
|
425
412
|
}
|
@@ -434,7 +421,7 @@ VALUE HttpParser_has_error(VALUE self)
|
|
434
421
|
VALUE HttpParser_is_finished(VALUE self)
|
435
422
|
{
|
436
423
|
puma_parser *http = NULL;
|
437
|
-
DATA_GET(self, puma_parser, http);
|
424
|
+
DATA_GET(self, puma_parser, &HttpParser_data_type, http);
|
438
425
|
|
439
426
|
return puma_parser_is_finished(http) ? Qtrue : Qfalse;
|
440
427
|
}
|
@@ -450,7 +437,7 @@ VALUE HttpParser_is_finished(VALUE self)
|
|
450
437
|
VALUE HttpParser_nread(VALUE self)
|
451
438
|
{
|
452
439
|
puma_parser *http = NULL;
|
453
|
-
DATA_GET(self, puma_parser, http);
|
440
|
+
DATA_GET(self, puma_parser, &HttpParser_data_type, http);
|
454
441
|
|
455
442
|
return INT2FIX(http->nread);
|
456
443
|
}
|
@@ -463,15 +450,16 @@ VALUE HttpParser_nread(VALUE self)
|
|
463
450
|
*/
|
464
451
|
VALUE HttpParser_body(VALUE self) {
|
465
452
|
puma_parser *http = NULL;
|
466
|
-
DATA_GET(self, puma_parser, http);
|
453
|
+
DATA_GET(self, puma_parser, &HttpParser_data_type, http);
|
467
454
|
|
468
455
|
return http->body;
|
469
456
|
}
|
470
457
|
|
471
|
-
|
458
|
+
#ifdef HAVE_OPENSSL_BIO_H
|
472
459
|
void Init_mini_ssl(VALUE mod);
|
460
|
+
#endif
|
473
461
|
|
474
|
-
void Init_puma_http11()
|
462
|
+
void Init_puma_http11(void)
|
475
463
|
{
|
476
464
|
|
477
465
|
VALUE mPuma = rb_define_module("Puma");
|
@@ -481,7 +469,7 @@ void Init_puma_http11()
|
|
481
469
|
DEF_GLOBAL(request_uri, "REQUEST_URI");
|
482
470
|
DEF_GLOBAL(fragment, "FRAGMENT");
|
483
471
|
DEF_GLOBAL(query_string, "QUERY_STRING");
|
484
|
-
DEF_GLOBAL(
|
472
|
+
DEF_GLOBAL(server_protocol, "SERVER_PROTOCOL");
|
485
473
|
DEF_GLOBAL(request_path, "REQUEST_PATH");
|
486
474
|
|
487
475
|
eHttpParserError = rb_define_class_under(mPuma, "HttpParserError", rb_eIOError);
|
@@ -498,6 +486,7 @@ void Init_puma_http11()
|
|
498
486
|
rb_define_method(cHttpParser, "body", HttpParser_body, 0);
|
499
487
|
init_common_fields();
|
500
488
|
|
501
|
-
|
489
|
+
#ifdef HAVE_OPENSSL_BIO_H
|
502
490
|
Init_mini_ssl(mPuma);
|
491
|
+
#endif
|
503
492
|
}
|
data/lib/puma/app/status.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
require_relative '../json_serialization'
|
2
3
|
|
3
4
|
module Puma
|
4
5
|
module App
|
@@ -7,58 +8,71 @@ module Puma
|
|
7
8
|
class Status
|
8
9
|
OK_STATUS = '{ "status": "ok" }'.freeze
|
9
10
|
|
10
|
-
|
11
|
-
|
11
|
+
# @param launcher [::Puma::Launcher]
|
12
|
+
# @param token [String, nil] the token used for authentication
|
13
|
+
#
|
14
|
+
def initialize(launcher, token = nil)
|
15
|
+
@launcher = launcher
|
12
16
|
@auth_token = token
|
13
17
|
end
|
14
18
|
|
19
|
+
# most commands call methods in `::Puma::Launcher` based on command in
|
20
|
+
# `env['PATH_INFO']`
|
15
21
|
def call(env)
|
16
22
|
unless authenticate(env)
|
17
23
|
return rack_response(403, 'Invalid auth token', 'text/plain')
|
18
24
|
end
|
19
25
|
|
20
|
-
|
21
|
-
|
22
|
-
|
26
|
+
# resp_type is processed by following case statement, return
|
27
|
+
# is a number (status) or a string used as the body of a 200 response
|
28
|
+
resp_type =
|
29
|
+
case env['PATH_INFO'][/\/([^\/]+)$/, 1]
|
30
|
+
when 'stop'
|
31
|
+
@launcher.stop ; 200
|
23
32
|
|
24
|
-
|
25
|
-
|
26
|
-
@cli.stop
|
27
|
-
rack_response(200, OK_STATUS)
|
33
|
+
when 'halt'
|
34
|
+
@launcher.halt ; 200
|
28
35
|
|
29
|
-
|
30
|
-
|
31
|
-
rack_response(200, OK_STATUS)
|
36
|
+
when 'restart'
|
37
|
+
@launcher.restart ; 200
|
32
38
|
|
33
|
-
|
34
|
-
|
35
|
-
rack_response(200, OK_STATUS)
|
39
|
+
when 'phased-restart'
|
40
|
+
@launcher.phased_restart ? 200 : 404
|
36
41
|
|
37
|
-
|
38
|
-
|
39
|
-
rack_response(404, '{ "error": "phased restart not available" }')
|
40
|
-
else
|
41
|
-
rack_response(200, OK_STATUS)
|
42
|
-
end
|
42
|
+
when 'refork'
|
43
|
+
@launcher.refork ? 200 : 404
|
43
44
|
|
44
|
-
|
45
|
-
|
46
|
-
rack_response(404, '{ "error": "reload_worker_directory not available" }')
|
47
|
-
else
|
48
|
-
rack_response(200, OK_STATUS)
|
49
|
-
end
|
45
|
+
when 'reload-worker-directory'
|
46
|
+
@launcher.send(:reload_worker_directory) ? 200 : 404
|
50
47
|
|
51
|
-
|
52
|
-
|
53
|
-
rack_response(200, OK_STATUS)
|
48
|
+
when 'gc'
|
49
|
+
GC.start ; 200
|
54
50
|
|
55
|
-
|
56
|
-
|
51
|
+
when 'gc-stats'
|
52
|
+
Puma::JSONSerialization.generate GC.stat
|
53
|
+
|
54
|
+
when 'stats'
|
55
|
+
Puma::JSONSerialization.generate @launcher.stats
|
56
|
+
|
57
|
+
when 'thread-backtraces'
|
58
|
+
backtraces = []
|
59
|
+
@launcher.thread_status do |name, backtrace|
|
60
|
+
backtraces << { name: name, backtrace: backtrace }
|
61
|
+
end
|
62
|
+
Puma::JSONSerialization.generate backtraces
|
63
|
+
|
64
|
+
else
|
65
|
+
return rack_response(404, "Unsupported action", 'text/plain')
|
66
|
+
end
|
57
67
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
68
|
+
case resp_type
|
69
|
+
when String
|
70
|
+
rack_response 200, resp_type
|
71
|
+
when 200
|
72
|
+
rack_response 200, OK_STATUS
|
73
|
+
when 404
|
74
|
+
str = env['PATH_INFO'][/\/(\S+)/, 1].tr '-', '_'
|
75
|
+
rack_response 404, "{ \"error\": \"#{str} not available\" }"
|
62
76
|
end
|
63
77
|
end
|
64
78
|
|
@@ -71,8 +85,8 @@ module Puma
|
|
71
85
|
|
72
86
|
def rack_response(status, body, content_type='application/json')
|
73
87
|
headers = {
|
74
|
-
'
|
75
|
-
'
|
88
|
+
'content-type' => content_type,
|
89
|
+
'content-length' => body.bytesize.to_s
|
76
90
|
}
|
77
91
|
|
78
92
|
[status, headers, [body]]
|