summaryrefslogtreecommitdiff
path: root/UPGRADE-GUIDE.md
blob: 5d62f07bd318a65fb2112764b06f9273d458f6b0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# Rack 3 Upgrade Guide

This document is a work in progress, but outlines some of the key changes in
Rack 3 which you should be aware of in order to update your server, middleware
and/or applications.

## Interface Changes

### Rack 2 & Rack 3 compatibility

Most applications can be compatible with Rack 2 and 3 by following the strict intersection of the Rack Specifications, notably:

- Response array must now be non-frozen.
- Response `status` must now be an integer greater than or equal to 100.
- Response `headers` must now be an unfrozen hash.
- Response header keys can no longer include uppercase characters.
- `rack.input` is no longer required to be rewindable.
- `rack.multithread`/`rack.multiprocess`/`rack.run_once`/`rack.version` are no longer required environment keys.
- `rack.hijack?` (partial hijack) and `rack.hijack` (full hijack) are now independently optional.
- `rack.hijack_io` has been removed completely.
- `SERVER_PROTOCOL` is now a required key, matching the HTTP protocol used in the request.
- Middleware must no longer call `#each` on the body, but they can call `#to_ary` on the body if it responds to `#to_ary`.

There is one changed feature in Rack 3 which is not backwards compatible:

- Response header values can be an `Array` to handle multiple values (and no longer supports `\n` encoded headers).

You can achieve compatibility by using `Rack::Response#add_header` which provides an interface for adding headers without concern for the underlying format.

There is one new feature in Rack 3 which is not directly backwards compatible:

- Response body can now respond to `#call` (streaming body) instead of `#each` (enumerable body), for the equivalent of response hijacking in previous versions.

If supported by your server, you can use partial rack hijack instead (or wrap this behaviour in a middleware).

### `config.ru` `Rack::Builder#run` now accepts block

Previously, `Rack::Builder#run` method would only accept a callable argument:

```ruby
run lambda{|env| [200, {}, ["Hello World"]]}
```

This can be rewritten more simply:

```ruby
run do |env|
  [200, {}, ["Hello World"]]
end
```

### Response bodies can be used for bi-directional streaming

Previously, the `rack.hijack` response header could be used for implementing
bi-directional streaming (e.g. WebSockets).

```ruby
def call(env)
  stream_callback = proc do |stream|
    stream.read(...)
    stream.write(...)
  ensure
    stream.close(...)
  end

  return [200, {'rack.hijack' => stream_callback}, []]
end
```

This feature was optional and tricky to use correctly. You can now achieve the
same thing by giving `stream_callback` as the response body:

```ruby
def call(env)
  stream_callback = proc do |stream|
    stream.read(...)
    stream.write(...)
  ensure
    stream.close(...)
  end

  return [200, {}, stream_callback]
end
```

### `Rack::Session` was moved to a separate gem.

Previously, `Rack::Session` was part of the `rack` gem. Not every application
needs it, and it increases the security surface area of the `rack`, so it was
decided to extract it into its own gem `rack-session` which can be updated
independently.

Applications that make use of `rack-session` will need to add that gem as a
dependency:

```ruby
gem 'rack-session'
```

This provides all the previously available functionality.

### `bin/rackup`, `Rack::Server`, `Rack::Handler`and  `Rack::Lobster` were moved to a separate gem.

Previously, the `rackup` executable was included with Rack. Because WEBrick is
no longer a default gem with Ruby, we had to make a decision: either `rack`
should depend on `webrick` or we should move that functionality into a
separate gem. We chose the latter which will hopefully allow us to innovate
more rapidly on the design and implementation of `rackup` separately from
"rack the interface".

In Rack 3, you will need to include:

```ruby
gem 'rackup'
```

This provides all the previously available functionality.

The classes `Rack::Server`, `Rack::Handler` and  `Rack::Lobster` have been moved to the rackup gem too and renamed to `Rackup::Server`, `Rackup::Handler` and  `Rackup::Lobster` respectively.

To start an app with `Rackup::Server` with Rack 3 :

```ruby
require 'rackup'
Rackup::Server.start app: app, Port: 3000
```

#### `config.ru` autoloading is disabled unless `require 'rack'`

Previously, rack modules like `rack/directory` were autoloaded because `rackup` did require 'rack'. In Rack 3, you will need to write `require 'rack'` or require specific module explicitly.

```diff
+require 'rack'
run Rack::Directory.new '.'
```

or

```diff
+require 'rack/directory'
run Rack::Directory.new '.'
```

## Request Changes

### `rack.version` is no longer required

Previously, the "rack protocol version" was available in `rack.version` but it
was not practically useful, so it has been removed as a requirement.

### `rack.multithread`/`rack.multiprocess`/`rack.run_once` are no longer required

Previously, servers tried to provide these keys to reflect the execution
environment. These come too late to be useful, so they have been removed as  a
requirement.

### `rack.hijack?` now only applies to partial hijack

Previously, both full and partial hijiack were controlled by the presence and
value of `rack.hijack?`. Now, it only applies to partial hijack (which now can
be replaced by streaming bodies).

### `rack.hijack` alone indicates that you can execute a full hijack

Previously, `rack.hijack?` had to be truthy, as well as having `rack.hijack`
present in the request environment. Now, the presence of the `rack.hijack`
callback is enough.

### `rack.hijack_io` is removed

Previously, the server would try to set `rack.hijack_io` into the request
environment when `rack.hijack` was invoked for a full hijack. This was often
impossible if a middleware had called `env.dup`, so this requirement has been
dropped entirely.

### `rack.input` is no longer required to be rewindable

Previously, `rack.input` was required to be rewindable, i.e. `io.seek(0)` but
this was only generally possible with a file based backing, which prevented
efficient streaming of request bodies. Now, `rack.input` is not required to be
rewindable.

## Response Changes

### Response must be mutable

Rack 3 requires the response Array `[status, headers, body]` to be mutable.
Existing code that uses a frozen response will need to be changed:

```ruby
NOT_FOUND = [404, {}, ["Not Found"]].freeze

def call(env)
  ...
  return NOT_FOUND
end
```

should be rewritten as:

```ruby
def not_found
  [404, {}, ["Not Found"]]
end

def call(env)
  ...
  return not_found
end
```

Note there is a subtle bug in the former version: the headers hash is mutable
and can be modified, and these modifications can leak into subsequent requests.

### Response headers must be a mutable hash

Rack 3 requires response headers to be a mutable hash. Previously it could be
any object that would respond to `#each` and yield `key`/`value` pairs.
Previously, the following was acceptable:

```ruby
def call(env)
  return [200, [['content-type', 'text/plain']], ["Hello World"]]
end
```

Now you must use a hash instance:

```ruby
def call(env)
  return [200, {'content-type' => 'text/plain'}, ["Hello World"]]
end
```

This ensures middleware can predictably update headers as needed.

### Response Headers must be lower case

Rack 3 requires all response headers to be lower case. This is to simplify
fetching and updating response headers. Previously you had to use something like
`Rack::HeadersHash`

```ruby
def call(env)
  response = @app.call(env)
  # HeaderHash must allocate internal objects and compute lower case keys:
  headers = Rack::Utils::HeaderHash[response[1]]

  cache_response(headers['ETag'], response)

  ...
end
```

but now you must just use the normal form for HTTP header:

```ruby
def call(env)
  response = @app.call(env)
  # A plain hash with lower case keys:
  headers = response[1]

  cache_response(headers['etag'], response)

  ...
end
```

If you want your code to work with Rack 3 without having to manually lowercase
each header key used, instead of using a plain hash for headers, you can use
`Rack::Headers` on Rack 3.

```ruby
  headers = defined?(Rack::Headers) ? Rack::Headers.new : {}
```

`Rack::Headers` is a subclass of Hash that will automatically lowercase keys:

```ruby
  headers = Rack::Headers.new
  headers['Foo'] = 'bar'
  headers['FOO'] # => 'bar'
  headers.keys   # => ['foo']
```

### Multiple response header values are encoded using an `Array`

Response header values can be an Array to handle multiple values (and no longer
supports `\n` encoded headers). If you use `Rack::Response`, you don't need to
do anything, but if manually append values to response headers, you will need to
promote them to an Array, e.g.

```ruby
def set_cookie_header!(headers, key, value)
  if header = headers[SET_COOKIE]
    if header.is_a?(Array)
      header << set_cookie_header(key, value)
    else
      headers[SET_COOKIE] = [header, set_cookie_header(key, value)]
    end
  else
    headers[SET_COOKIE] = set_cookie_header(key, value)
  end
end
```

### Response body might not respond to `#each`

Rack 3 has more strict requirements on response bodies. Previously, response
body would only need to respond to `#each` and optionally `#close`. In addition,
there was no way to determine whether it was safe to call `#each` and buffer the
response.

### Response bodies can be buffered if they expose `#to_ary`

If your body responds to `#to_ary` then it must return an `Array` whose contents
are identical to that produced by calling `#each`. If the body responds to both
`#to_ary` and `#close` then its implementation of `#to_ary` must also call
`#close`.

Previously, it was not possible to determine whether a response body was
immediately available (could be buffered) or was streaming chunks. This case is
now unambiguously exposed by `#to_ary`:

```ruby
def call(env)
  status, headers, body = @app.call(env)

  # Check if we can buffer the body into an Array, so we can compute a digest:
  if body.respond_to?(:to_ary)
    body = body.to_ary
    digest = digest_body(body)
    headers[ETAG_STRING] = %(W/"#{digest}") if digest
  end

  return [status, headers, body]
end
```

### Middleware should not directly modify the response body

Be aware that the response body might not respond to `#each` and you must now
check if the body responds to `#each` or not to determine if it is an enumerable
or streaming body.

You must not call `#each` directly on the body and instead you should return a
new body that calls `#each` on the original body.

### Status needs to be an `Integer`

The response status is now required to be an `Integer` with a value greater or equal to 100.

Previously any object that responded to `#to_i` was allowed, so a response like `["200", {}, ""]` will need to be replaced with `[200, {}, ""]` and so on. This can be done by calling `#to_i` on the status object yourself.