startback 1.2.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +815 -0
- data/README.md +3 -2
- data/UPGRADING.md +342 -0
- data/lib/startback/caching/entity_cache.rb +2 -2
- data/lib/startback/security/rate_limiter.rb +1 -1
- data/lib/startback/version.rb +3 -3
- data/lib/startback/web/auto_caching.rb +10 -4
- data/lib/startback/web/cors_headers.rb +8 -2
- data/lib/startback/web/health_check.rb +1 -1
- data/spec/spec_helper.rb +6 -0
- data/spec/unit/web/test_auto_caching.rb +19 -0
- data/spec/unit/web/test_cors_headers.rb +21 -0
- metadata +140 -58
data/README.md
CHANGED
|
@@ -14,10 +14,11 @@ Currently,
|
|
|
14
14
|
|
|
15
15
|
## Public API
|
|
16
16
|
|
|
17
|
-
This gem uses
|
|
18
|
-
API is defined as follows:
|
|
17
|
+
This gem uses semantic versioning. The public API is defined as follows:
|
|
19
18
|
|
|
20
19
|
* All ruby classes, require path, constructor arguments, and public methods.
|
|
21
20
|
|
|
22
21
|
* The `enspirit/startback:api` and `enspirit/startback:web` docker images and
|
|
23
22
|
main `CMD`.
|
|
23
|
+
|
|
24
|
+
Upgrading across a major version? See [UPGRADING.md](UPGRADING.md).
|
data/UPGRADING.md
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# Upgrading Startback
|
|
2
|
+
|
|
3
|
+
## From 1.2.x to 2.0.0
|
|
4
|
+
|
|
5
|
+
**Startback's own API has not changed.** Every class, require path, constructor
|
|
6
|
+
argument and public method behaves as it did in 1.2.x. What changed is the
|
|
7
|
+
dependency floor: Sinatra 4, and therefore Rack 3, are now required, and the
|
|
8
|
+
other dependencies moved to their latest major.
|
|
9
|
+
|
|
10
|
+
So almost everything below is about *your* application code meeting Rack 3 and
|
|
11
|
+
Sinatra 4, not about Startback. Each section is written as: what you will see,
|
|
12
|
+
why, and what to do.
|
|
13
|
+
|
|
14
|
+
Rough budget: a small API service usually needs **two changes** -- setting
|
|
15
|
+
`RACK_ENV`, and lowercasing any response triples it builds by hand. The rest
|
|
16
|
+
depends on what you use.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Before you start
|
|
21
|
+
|
|
22
|
+
| | |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Ruby | **>= 3.2** is now enforced by the gemspec. 3.1 is end of life. |
|
|
25
|
+
| webspicy | **Must move to 1.x.** See [webspicy](#10-webspicy-must-move-to-1x) -- this one fails to install, it does not degrade quietly. |
|
|
26
|
+
| Everything else | Installs fine; behaviour changes are listed below. |
|
|
27
|
+
|
|
28
|
+
Start with:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
bundle update startback
|
|
32
|
+
bundle exec rake test # or whatever runs your suite
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Most failures will be issue 1 or issue 2.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 1. Every request returns `403 Host not permitted`
|
|
40
|
+
|
|
41
|
+
**You will see** every request failing with status 403 and a `text/plain` body
|
|
42
|
+
reading `Host not permitted` -- in your test suite first, and in local
|
|
43
|
+
development if you reach the app through anything other than `localhost`.
|
|
44
|
+
|
|
45
|
+
**Why.** Sinatra 4.1 added `Rack::Protection::HostAuthorization` (for
|
|
46
|
+
CVE-2024-21510). In the `development` environment it only accepts `localhost`,
|
|
47
|
+
`*.localhost`, `*.test` and IP literals as `Host`. `development` is the
|
|
48
|
+
environment Sinatra picks when **neither `RACK_ENV` nor `APP_ENV` is set**,
|
|
49
|
+
which is the common case in test suites and docker-compose.
|
|
50
|
+
|
|
51
|
+
Test suites are hit systematically because `Rack::Test` sends requests to
|
|
52
|
+
`example.org`, and so does webspicy's `RackTestClient`.
|
|
53
|
+
|
|
54
|
+
**Production is not affected**: outside `development`, the permitted list is
|
|
55
|
+
empty, which means "allow everything".
|
|
56
|
+
|
|
57
|
+
**Fix, for test suites** -- set the environment before Sinatra is loaded, i.e.
|
|
58
|
+
at the very top of `spec_helper.rb` (or your webspicy `config.rb`), *above* the
|
|
59
|
+
`require`s:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
ENV['RACK_ENV'] ||= 'test'
|
|
63
|
+
|
|
64
|
+
require 'startback'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Fix, for local development behind a custom hostname** -- either set
|
|
68
|
+
`RACK_ENV` in your docker-compose/`.env`, or declare the hosts:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
class MyApi < Startback::Web::Api
|
|
72
|
+
set :host_authorization, { permitted_hosts: ['.my-app.internal', '.localhost'] }
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A leading dot matches subdomains. Passing an empty list disables the check
|
|
77
|
+
entirely -- reasonable for a service that only ever sits behind a trusted
|
|
78
|
+
reverse proxy, but it is opting out of a CVE fix, so do it deliberately.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 2. A response header appears twice, or a middleware stops seeing it
|
|
83
|
+
|
|
84
|
+
**You will see** responses carrying, say, both `Cache-Control` and
|
|
85
|
+
`cache-control` with different values; or a middleware that used to read a
|
|
86
|
+
header no longer finding it; or a caching proxy behaving oddly.
|
|
87
|
+
|
|
88
|
+
**Why.** The Rack 3 SPEC states that response header keys *"must not contain
|
|
89
|
+
uppercase ASCII characters (A-Z)"*. Rack 3 middleware therefore looks headers
|
|
90
|
+
up in lowercase. A triple you build by hand with `"Content-Type"` is a
|
|
91
|
+
different key from the `"content-type"` everything else uses, so instead of
|
|
92
|
+
overriding, it coexists.
|
|
93
|
+
|
|
94
|
+
Nothing raises. This is a silent behaviour change, which is what makes it worth
|
|
95
|
+
hunting for deliberately.
|
|
96
|
+
|
|
97
|
+
**Fix.** Lowercase the header names in any response triple your code builds:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# before
|
|
101
|
+
[200, { "Content-Type" => "application/json" }, [body]]
|
|
102
|
+
|
|
103
|
+
# after
|
|
104
|
+
[200, { "content-type" => "application/json" }, [body]]
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Grep for it:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
grep -rnE '"(Content-Type|Cache-Control|Location|Content-Length|X-[A-Za-z-]+)"\s*=>' app lib
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
You do **not** need to change:
|
|
114
|
+
|
|
115
|
+
* `content_type :json` and friends inside a Sinatra route -- Sinatra normalizes.
|
|
116
|
+
* Reading headers from a response object (`response['Content-Type']`) --
|
|
117
|
+
`Rack::Headers` is case-insensitive on read.
|
|
118
|
+
* Startback's own middlewares. `AutoCaching`, `CorsHeaders`, `HealthCheck`,
|
|
119
|
+
`Shield` and `CatchAll` were all fixed in this release; `AutoCaching` and
|
|
120
|
+
`CorsHeaders` had exactly this duplication bug.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## 3. `undefined method 'each' for an instance of String`
|
|
125
|
+
|
|
126
|
+
**You will see** that error, or a blank response body.
|
|
127
|
+
|
|
128
|
+
**Why.** Rack 3 requires a response body to respond to `each` or `call`. A bare
|
|
129
|
+
String is no longer a valid body.
|
|
130
|
+
|
|
131
|
+
**Fix.** Wrap it:
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
# before
|
|
135
|
+
[404, { "content-type" => "text/plain" }, "NotFound"]
|
|
136
|
+
|
|
137
|
+
# after
|
|
138
|
+
[404, { "content-type" => "text/plain" }, ["NotFound"]]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 4. `uninitialized constant` for a Rack 2 class
|
|
144
|
+
|
|
145
|
+
Rack 3 removed a number of constants. If your app or a third-party middleware
|
|
146
|
+
uses one, it fails at load time:
|
|
147
|
+
|
|
148
|
+
| Removed | Use instead |
|
|
149
|
+
|---|---|
|
|
150
|
+
| `Rack::Utils::HeaderHash` | `Rack::Headers` |
|
|
151
|
+
| `Rack::File` | `Rack::Files` |
|
|
152
|
+
| `Rack::Session::Cookie` | the `rack-session` gem (Sinatra already depends on it) |
|
|
153
|
+
| `Rack::Handler` | `Rackup::Handler`, from the `rackup` gem |
|
|
154
|
+
|
|
155
|
+
If the failure comes from a gem rather than your code, check whether it has a
|
|
156
|
+
Rack 3 compatible release. This is the most common reason an upgrade stalls,
|
|
157
|
+
and it is nothing Startback can shield you from.
|
|
158
|
+
|
|
159
|
+
Sinatra 4 also dropped the `IndifferentHash` initializer, disabled
|
|
160
|
+
`session_hijacking` protection by default, and removed
|
|
161
|
+
`Rack::Protection::EncryptedCookie` (cookies are still encrypted, by
|
|
162
|
+
`Rack::Session::Cookie`). And if you start the server by running the app file
|
|
163
|
+
directly rather than through `config.ru` + puma, you now need the `rackup` gem
|
|
164
|
+
in your Gemfile.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## 5. Puma: lifecycle hooks renamed, and a new default bind
|
|
169
|
+
|
|
170
|
+
Puma goes from 6 to 8, crossing two majors. Startback never loads puma itself
|
|
171
|
+
-- it ships it for you -- so nothing here is detectable by Startback's tests.
|
|
172
|
+
|
|
173
|
+
**Puma 7 renamed every lifecycle hook.** If your `puma.rb` uses the old names
|
|
174
|
+
they are simply not called, silently:
|
|
175
|
+
|
|
176
|
+
| Before | After |
|
|
177
|
+
|---|---|
|
|
178
|
+
| `on_worker_boot` | `before_worker_boot` |
|
|
179
|
+
| `on_worker_shutdown` | `before_worker_shutdown` |
|
|
180
|
+
| `on_restart` | `before_restart` |
|
|
181
|
+
| `on_booted` | `after_booted` |
|
|
182
|
+
| `on_stopped` | `after_stopped` |
|
|
183
|
+
| `on_refork` | `before_refork` |
|
|
184
|
+
| `on_thread_start` | `before_thread_start` |
|
|
185
|
+
|
|
186
|
+
This matters most for database connection handling, which is usually exactly
|
|
187
|
+
what those hooks do.
|
|
188
|
+
|
|
189
|
+
**Puma 7 also** made `preload_app!` the default in clustered mode, and requires
|
|
190
|
+
a config instance to be `clamp`-ed before values are read.
|
|
191
|
+
|
|
192
|
+
**Puma 8** changed the default production bind from `0.0.0.0` to `::` when an
|
|
193
|
+
IPv6 interface is available. In a container that publishes ports over IPv4
|
|
194
|
+
only, this can make the service unreachable. Bind explicitly if you care:
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
# puma.rb
|
|
198
|
+
bind 'tcp://0.0.0.0:3000'
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Not ready?** `gem 'puma', '~> 6.0'` in your Gemfile. Startback accepts
|
|
202
|
+
`>= 6.0.2, < 9.0`.
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## 6. `undefined method 'fast_generate' for module JSON`
|
|
207
|
+
|
|
208
|
+
**Why.** json 3 removed `JSON.fast_generate`.
|
|
209
|
+
|
|
210
|
+
**Fix.** `JSON.generate`. It is the same output; `fast_generate` only skipped
|
|
211
|
+
the circular-reference check.
|
|
212
|
+
|
|
213
|
+
Startback used it internally in `Security::RateLimiter` and
|
|
214
|
+
`Caching::EntityCache#encode_key`, and both now use `JSON.generate`. **The
|
|
215
|
+
generated strings are identical**, so cache entries and rate-limit counters
|
|
216
|
+
survive the upgrade -- no cache flush needed.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 7. jwt 2 to 3
|
|
221
|
+
|
|
222
|
+
Only relevant if your application uses JWT; Startback ships the gem but never
|
|
223
|
+
loads it. jwt 3 is a real break:
|
|
224
|
+
|
|
225
|
+
* RSA keys must be **at least 2048 bits**. Shorter keys now raise.
|
|
226
|
+
* Base64 decoding follows RFC 4648 strictly; tolerantly-encoded tokens that
|
|
227
|
+
used to decode now fail.
|
|
228
|
+
* The payload cannot be read before the signature is verified.
|
|
229
|
+
* `HS512256` is gone.
|
|
230
|
+
* Custom algorithms must include `JWT::JWA::SigningAlgorithm`.
|
|
231
|
+
* Since 3.3: if you rescue `JWT::DecodeError`, `JWT::IncorrectAlgorithm` or
|
|
232
|
+
`ArgumentError` **around `JWT.encode`**, rescue `JWT::EncodeError` instead.
|
|
233
|
+
Decoding is unaffected.
|
|
234
|
+
|
|
235
|
+
Read jwt's own `UPGRADING.md` before taking it. **Not ready?**
|
|
236
|
+
`gem 'jwt', '~> 2.1'`. Startback accepts `>= 2.1, < 4.0`.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 8. finitio 0.12 to 1.0: check your `.fio` schemas
|
|
241
|
+
|
|
242
|
+
Two removals affect schemas, and one of them changes meaning silently:
|
|
243
|
+
|
|
244
|
+
* `Fixnum` and `Bignum` are gone from `finitio/data`. Use `Integer`. This one
|
|
245
|
+
fails loudly.
|
|
246
|
+
* **`FalseClass` was a bug and is now fixed.** It used to be an alias of
|
|
247
|
+
`.TrueClass`, so it accepted `true` and rejected `false`. If a schema of
|
|
248
|
+
yours worked around that -- writing `FalseClass` where it meant a *true*
|
|
249
|
+
value -- it now means the opposite.
|
|
250
|
+
|
|
251
|
+
Grep before upgrading:
|
|
252
|
+
|
|
253
|
+
```sh
|
|
254
|
+
grep -rn "Fixnum\|Bignum\|FalseClass" --include=*.fio .
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Not ready?** `gem 'finitio', '~> 0.12'`. Startback accepts `>= 0.12, < 2.0`.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## 9. bunny 2 to 3, if you use the event bus
|
|
262
|
+
|
|
263
|
+
Applies to `Startback::Event::Bus::Bunny::Async` only.
|
|
264
|
+
|
|
265
|
+
* Versioned delivery tags are removed.
|
|
266
|
+
* Passive declarations (`passive: true`) are no longer replayed by topology
|
|
267
|
+
recovery.
|
|
268
|
+
* The `openssl` gem >= 3.3 is now required, which means a native build --
|
|
269
|
+
watch slim/alpine images.
|
|
270
|
+
|
|
271
|
+
**Heads up on coverage:** Startback's test matrix has no RabbitMQ, so the Bunny
|
|
272
|
+
bus is upgraded but *unverified by the suite*. If you use it, exercise it in a
|
|
273
|
+
staging environment rather than trusting the green build.
|
|
274
|
+
|
|
275
|
+
**Not ready?** `gem 'bunny', '~> 2.14'`. Startback accepts `>= 2.14, < 4.0`.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## 10. webspicy must move to 1.x
|
|
280
|
+
|
|
281
|
+
**You will see** `bundle install` fail outright:
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
Because every version of startback depends on rack-robustness >= 2.0, < 3.0
|
|
285
|
+
and webspicy >= 0.25.0 depends on rack-robustness >= 1.2, < 2.0,
|
|
286
|
+
every version of startback is incompatible with webspicy >= 0.25.0.
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Why.** Every webspicy 0.27.x release caps `finitio < 0.13`, `http < 6.0` and
|
|
290
|
+
`rack-robustness < 2.0`. webspicy 1.0 widened all three.
|
|
291
|
+
|
|
292
|
+
**Fix.** `gem 'webspicy', '>= 1.0', '< 2.0'`.
|
|
293
|
+
|
|
294
|
+
There is no way around this one, and no quiet degradation: bundler refuses to
|
|
295
|
+
resolve.
|
|
296
|
+
|
|
297
|
+
Coming from 0.26 or earlier, note that webspicy validates unstructured
|
|
298
|
+
response bodies against `output_schema` since 0.27, where it used to skip
|
|
299
|
+
them. A content-negotiating endpoint may need its schema widened accordingly
|
|
300
|
+
(e.g. `[Todo]|Csv` for one serving both JSON and CSV).
|
|
301
|
+
|
|
302
|
+
---
|
|
303
|
+
|
|
304
|
+
## Opting out, gem by gem
|
|
305
|
+
|
|
306
|
+
Startback deliberately declares **wide ranges** for the dependencies it does
|
|
307
|
+
not use itself, so you can stay on an older major while still upgrading
|
|
308
|
+
Startback. Add the pin to your own Gemfile:
|
|
309
|
+
|
|
310
|
+
| Gem | Startback accepts | Pin to stay put |
|
|
311
|
+
|---|---|---|
|
|
312
|
+
| puma | `>= 6.0.2, < 9.0` | `gem 'puma', '~> 6.0'` |
|
|
313
|
+
| jwt | `>= 2.1, < 4.0` | `gem 'jwt', '~> 2.1'` |
|
|
314
|
+
| http | `>= 5.0, < 7.0` | `gem 'http', '~> 5.0'` |
|
|
315
|
+
| bunny | `>= 2.14, < 4.0` | `gem 'bunny', '~> 2.14'` |
|
|
316
|
+
| finitio | `>= 0.12, < 2.0` | `gem 'finitio', '~> 0.12'` |
|
|
317
|
+
| json | `>= 2.6, < 4.0` | `gem 'json', '~> 2.6'` |
|
|
318
|
+
|
|
319
|
+
These cannot be opted out of, because Startback's own code depends on them:
|
|
320
|
+
|
|
321
|
+
| Gem | Required |
|
|
322
|
+
|---|---|
|
|
323
|
+
| sinatra | `>= 4.0, < 5.0` -- the middlewares use `Rack::Headers`, Rack 3 only |
|
|
324
|
+
| rack-robustness | `>= 2.0, < 3.0` -- `Shield` and `CatchAll` subclass it |
|
|
325
|
+
| Ruby | `>= 3.2` |
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Checklist
|
|
330
|
+
|
|
331
|
+
- [ ] Ruby >= 3.2
|
|
332
|
+
- [ ] `RACK_ENV` set in the test suite, at the top of `spec_helper.rb`
|
|
333
|
+
- [ ] Response triples built by hand use lowercase header names
|
|
334
|
+
- [ ] Response bodies are arrays, not bare Strings
|
|
335
|
+
- [ ] No `Rack::Utils::HeaderHash` / `Rack::File` / `Rack::Handler` left, in
|
|
336
|
+
your code or your gems
|
|
337
|
+
- [ ] `puma.rb` lifecycle hooks renamed; bind address checked
|
|
338
|
+
- [ ] No `JSON.fast_generate` left
|
|
339
|
+
- [ ] `.fio` schemas grepped for `Fixnum`, `Bignum`, `FalseClass`
|
|
340
|
+
- [ ] webspicy on 1.x
|
|
341
|
+
- [ ] jwt / bunny reviewed, or pinned
|
|
342
|
+
- [ ] Bunny event bus exercised somewhere real, since CI does not cover it
|
|
@@ -144,10 +144,10 @@ module Startback
|
|
|
144
144
|
|
|
145
145
|
# Encodes a context free key to an actual cache key.
|
|
146
146
|
#
|
|
147
|
-
# Default implementation uses JSON.
|
|
147
|
+
# Default implementation uses JSON.generate but MAY be
|
|
148
148
|
# overriden.
|
|
149
149
|
def encode_key(context_free_key)
|
|
150
|
-
JSON.
|
|
150
|
+
JSON.generate(context_free_key)
|
|
151
151
|
end
|
|
152
152
|
|
|
153
153
|
# Returns whether `cached` entity seems fresh enough to
|
data/lib/startback/version.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'rack'
|
|
2
|
+
|
|
1
3
|
module Startback
|
|
2
4
|
module Web
|
|
3
5
|
#
|
|
@@ -53,8 +55,12 @@ module Startback
|
|
|
53
55
|
|
|
54
56
|
protected
|
|
55
57
|
|
|
58
|
+
# Rack::Headers is used so that the defaults set here are actually
|
|
59
|
+
# overriden by the downstream application, whatever the case it uses
|
|
60
|
+
# for its own header names.
|
|
56
61
|
def patch_response_headers(hs)
|
|
57
|
-
|
|
62
|
+
defaults = development? ? @cache_headers[:development] : @cache_headers[:production]
|
|
63
|
+
Rack::Headers[defaults].merge(hs)
|
|
58
64
|
end
|
|
59
65
|
|
|
60
66
|
def development?
|
|
@@ -68,16 +74,16 @@ module Startback
|
|
|
68
74
|
def default_headers
|
|
69
75
|
{
|
|
70
76
|
development: {
|
|
71
|
-
"
|
|
77
|
+
"cache-control" => DEVELOPMENT_CACHE_CONTROL
|
|
72
78
|
},
|
|
73
79
|
production: {
|
|
74
|
-
"
|
|
80
|
+
"cache-control" => PRODUCTION_CACHE_CONTROL
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
end
|
|
78
84
|
|
|
79
85
|
def normalize_headers(h)
|
|
80
|
-
Hash[h.map{|k,v| [k, v.is_a?(Hash) ? v : {"
|
|
86
|
+
Hash[h.map{|k,v| [k, v.is_a?(Hash) ? v : {"cache-control" => v} ] }]
|
|
81
87
|
end
|
|
82
88
|
|
|
83
89
|
end # class AutoCaching
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'rack'
|
|
2
|
+
|
|
1
3
|
module Startback
|
|
2
4
|
module Web
|
|
3
5
|
#
|
|
@@ -62,7 +64,8 @@ module Startback
|
|
|
62
64
|
headers = cors_headers(origin).merge(headers)
|
|
63
65
|
end
|
|
64
66
|
if env['REQUEST_METHOD'] == 'OPTIONS'
|
|
65
|
-
headers
|
|
67
|
+
headers = Rack::Headers[headers]
|
|
68
|
+
headers['content-length'] = '0'
|
|
66
69
|
status, headers, body = [204, headers, []]
|
|
67
70
|
end
|
|
68
71
|
[status, headers, body]
|
|
@@ -70,8 +73,11 @@ module Startback
|
|
|
70
73
|
|
|
71
74
|
private
|
|
72
75
|
|
|
76
|
+
# Rack::Headers is used so that the CORS headers set here are actually
|
|
77
|
+
# overriden by the downstream application, whatever the case it uses
|
|
78
|
+
# for its own header names.
|
|
73
79
|
def cors_headers(origin)
|
|
74
|
-
headers = @options[:headers]
|
|
80
|
+
headers = Rack::Headers[@options[:headers]]
|
|
75
81
|
if bounce = do_bounce(origin)
|
|
76
82
|
headers['Access-Control-Allow-Origin'] = bounce
|
|
77
83
|
else
|
data/spec/spec_helper.rb
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
# Sinatra 4 restricts the Host header to localhost-like values in the
|
|
2
|
+
# `development` environment, which is the one used when RACK_ENV is unset.
|
|
3
|
+
# Rack::Test issues requests against `example.org`, hence the need to be
|
|
4
|
+
# explicit about running in test mode here.
|
|
5
|
+
ENV["RACK_ENV"] ||= "test"
|
|
6
|
+
|
|
1
7
|
require 'startback'
|
|
2
8
|
require 'startback/caching'
|
|
3
9
|
require 'startback/event'
|
|
@@ -76,6 +76,25 @@ module Startback
|
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
context 'when a lowercase cache-control header is already set by the app' do
|
|
80
|
+
# This is what Rack 3 applications actually emit, Sinatra 4 included.
|
|
81
|
+
# The raw response triple is inspected here on purpose: Rack::Test
|
|
82
|
+
# normalizes header names on read, and would hide a duplicate.
|
|
83
|
+
subject do
|
|
84
|
+
app = Rack::Builder.new do
|
|
85
|
+
use AutoCaching
|
|
86
|
+
run ->(env){ [200, {"cache-control" => "priority"}, ["Hello error"]] }
|
|
87
|
+
end.to_app
|
|
88
|
+
_, headers, _ = app.call(Rack::MockRequest.env_for("/"))
|
|
89
|
+
headers
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'lets the application win, and does not duplicate the header' do
|
|
93
|
+
expect(subject.keys.grep(/cache-control/i)).to eql(["cache-control"])
|
|
94
|
+
expect(subject["cache-control"]).to eql("priority")
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
79
98
|
end # CatchAll
|
|
80
99
|
end # module Web
|
|
81
100
|
end # module Startback
|
|
@@ -134,6 +134,27 @@ module Startback
|
|
|
134
134
|
end
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
+
context 'when the app sets specific headers in lowercase' do
|
|
138
|
+
# This is what Rack 3 applications actually emit, Sinatra 4 included.
|
|
139
|
+
# The raw response triple is inspected here on purpose: Rack::Test
|
|
140
|
+
# normalizes header names on read, and would hide a duplicate.
|
|
141
|
+
subject do
|
|
142
|
+
app = Rack::Builder.new do
|
|
143
|
+
use CorsHeaders
|
|
144
|
+
run ->(env){ [200, {'access-control-allow-methods' => "POST"}, ["Hello world"]] }
|
|
145
|
+
end.to_app
|
|
146
|
+
env = Rack::MockRequest.env_for("/", "HTTP_ORIGIN" => "https://test.com")
|
|
147
|
+
_, headers, _ = app.call(env)
|
|
148
|
+
headers
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it 'does not override them, and does not duplicate them either' do
|
|
152
|
+
expect(subject.keys.grep(/access-control-allow-methods/i))
|
|
153
|
+
.to eql(["access-control-allow-methods"])
|
|
154
|
+
expect(subject["access-control-allow-methods"]).to eql("POST")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
137
158
|
end # CatchAll
|
|
138
159
|
end # module Web
|
|
139
160
|
end # module Startback
|