Skip to content

Commit 0a7ed5e

Browse files
Separate internal error redirections.
Assisted-By: devx/b3414b50-d642-461b-95a8-8f773c4d087b
1 parent 8dbae72 commit 0a7ed5e

11 files changed

Lines changed: 132 additions & 83 deletions

File tree

context/middleware.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use Utopia::Static,
1818

1919
## Redirection
2020

21-
The {ruby Utopia::Redirection} middleware is used for redirecting requests based on patterns and status codes.
21+
The {ruby Utopia::Redirection} middleware is used for redirecting requests based on paths.
2222

2323
~~~ ruby
2424
use Utopia::Redirection do |redirects|
@@ -30,12 +30,16 @@ use Utopia::Redirection do |redirects|
3030

3131
# Redirect matching path prefixes:
3232
redirects.moved '/old/', '/new/'
33-
34-
# Redirect error status codes to internal error documents:
35-
redirects.error 404, '/errors/file-not-found'
3633
end
3734
~~~
3835

36+
The {ruby Utopia::Redirection::Errors} middleware maps unhandled error responses to internal error documents. It retains the original response status and does not issue a client-visible redirect:
37+
38+
~~~ ruby
39+
use Utopia::Redirection::Errors,
40+
404 => '/errors/file-not-found'
41+
~~~
42+
3943
## Localization
4044

4145
The {ruby Utopia::Localization} middleware computes immutable localization preferences from the request path, host, and `accept-language` header. Localization-aware resource middleware, including {ruby Utopia::Static} and {ruby Utopia::Content}, resolves those preferences without invoking controllers more than once. Place the localization middleware before those resources in the middleware stack.

guides/middleware/readme.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use Utopia::Static,
1818

1919
## Redirection
2020

21-
The {ruby Utopia::Redirection} middleware is used for redirecting requests based on patterns and status codes.
21+
The {ruby Utopia::Redirection} middleware is used for redirecting requests based on paths.
2222

2323
~~~ ruby
2424
use Utopia::Redirection do |redirects|
@@ -30,12 +30,16 @@ use Utopia::Redirection do |redirects|
3030

3131
# Redirect matching path prefixes:
3232
redirects.moved '/old/', '/new/'
33-
34-
# Redirect error status codes to internal error documents:
35-
redirects.error 404, '/errors/file-not-found'
3633
end
3734
~~~
3835

36+
The {ruby Utopia::Redirection::Errors} middleware maps unhandled error responses to internal error documents. It retains the original response status and does not issue a client-visible redirect:
37+
38+
~~~ ruby
39+
use Utopia::Redirection::Errors,
40+
404 => '/errors/file-not-found'
41+
~~~
42+
3943
## Localization
4044

4145
The {ruby Utopia::Localization} middleware computes immutable localization preferences from the request path, host, and `accept-language` header. Localization-aware resource middleware, including {ruby Utopia::Static} and {ruby Utopia::Content}, resolves those preferences without invoking controllers more than once. Place the localization middleware before those resources in the middleware stack.

lib/utopia/redirection.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
# Copyright, 2009-2026, by Samuel Williams.
55

66
require_relative "redirection/request_failure"
7+
require_relative "redirection/errors"
78
require_relative "redirection/rule"
89
require_relative "redirection/builder"
910
require_relative "redirection/middleware"

lib/utopia/redirection/builder.rb

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,12 @@ class Builder
1313
# Initialize an empty redirection configuration.
1414
def initialize
1515
@rules = []
16-
@errors = {}
1716
end
1817

1918
# The configured request redirection rules.
2019
# @returns [Array] The rules in declaration order.
2120
attr :rules
2221

23-
# The configured error document paths indexed by response status.
24-
# @returns [Hash(Integer, String)] The configured error documents.
25-
attr :errors
26-
2722
# Configure and freeze this builder.
2823
# @yields {|builder| ...} The redirection configuration.
2924
# @returns [self] This builder.
@@ -47,7 +42,6 @@ def freeze
4742
return self if frozen?
4843

4944
@rules.freeze
50-
@errors.freeze
5145

5246
return super
5347
end
@@ -108,15 +102,6 @@ def moved(pattern, prefix, status: 301, flatten: false, max_age: MAX_AGE)
108102
return self
109103
end
110104

111-
# Replace an unhandled response status with an internal error document.
112-
# @parameter status [Integer] The response status to handle.
113-
# @parameter path [String] The internal error document path.
114-
# @returns [self] This builder.
115-
def error(status, path)
116-
@errors[status] = path
117-
return self
118-
end
119-
120105
private
121106

122107
def add(status, max_age, &resolver)

lib/utopia/redirection/errors.rb

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# frozen_string_literal: true
2+
3+
# Released under the MIT License.
4+
# Copyright, 2009-2026, by Samuel Williams.
5+
6+
require_relative "../middleware"
7+
require_relative "../request"
8+
require_relative "../response"
9+
require_relative "request_failure"
10+
11+
module Utopia
12+
module Redirection
13+
# Performs internal redirections for unhandled error responses.
14+
class Errors < Protocol::HTTP::Middleware
15+
# Initialize internal error redirections.
16+
# @parameter delegate [Protocol::HTTP::Middleware] The downstream middleware.
17+
# @parameter codes [Hash(Integer, String)] The internal path for each error status.
18+
def initialize(delegate, codes = {})
19+
super(delegate)
20+
21+
@codes = codes
22+
end
23+
24+
# Freeze this object and its internal state.
25+
# @returns [self] This object.
26+
def freeze
27+
return self if frozen?
28+
29+
@codes.freeze
30+
31+
return super
32+
end
33+
34+
# Check whether the response status requires error handling.
35+
# @parameter response [Protocol::HTTP::Response] The response.
36+
# @returns [Boolean] Whether the response is an error without handler-provided headers.
37+
def unhandled_error?(response)
38+
response.status >= 400 && response.headers.empty?
39+
end
40+
41+
# Replace an unhandled error response with its configured error document.
42+
# @parameter request [Utopia::Request] The request.
43+
# @parameter response [Protocol::HTTP::Response] The unhandled error response.
44+
# @parameter location [String] The configured error document path.
45+
# @returns [Protocol::HTTP::Response] The error-document response.
46+
# @raises [RequestFailure] If the configured error document also fails.
47+
def replace_error(request, response, location)
48+
resource_status = response.status
49+
50+
# The original response is replaced by the configured error document:
51+
response.close
52+
53+
error_request = request.with(method: "GET", path_info: location)
54+
error_response = Response.wrap(@delegate.call(error_request))
55+
56+
if error_response.status >= 400
57+
error = RequestFailure.new(request.path_info, resource_status, location, error_response.status)
58+
59+
# The failed error document will not be returned to the server:
60+
error_response.close(error)
61+
62+
raise error
63+
end
64+
65+
# Feed the error code back with the error document:
66+
error_response.status = resource_status
67+
return error_response
68+
end
69+
70+
# Replace configured unhandled responses through an internal request.
71+
# @parameter request [Utopia::Request] The request.
72+
# @returns [Protocol::HTTP::Response] The original or error-document response.
73+
def call(request)
74+
response = Response.wrap(@delegate.call(request))
75+
76+
if unhandled_error?(response) && location = @codes[response.status]
77+
return replace_error(request, response, location)
78+
end
79+
80+
return response
81+
end
82+
end
83+
end
84+
end

lib/utopia/redirection/middleware.rb

Lines changed: 3 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
module Utopia
1111
module Redirection
12-
# Applies configured request redirections and error documents.
12+
# Applies configured request redirections.
1313
class Middleware < Protocol::HTTP::Middleware
1414
# Initialize redirection handling.
1515
# @parameter delegate [Protocol::HTTP::Middleware] The downstream middleware.
@@ -18,7 +18,6 @@ def initialize(delegate, builder)
1818
super(delegate)
1919

2020
@rules = builder.rules
21-
@errors = builder.errors
2221
end
2322

2423
# Build a redirect response for the given rule and location.
@@ -34,43 +33,7 @@ def redirect(rule, location)
3433
return Response[rule.status, headers, []]
3534
end
3635

37-
# Check whether the response status requires error handling.
38-
# @parameter response [Protocol::HTTP::Response] The response.
39-
# @returns [Boolean] Whether the response is an error without handler-provided headers.
40-
def unhandled_error?(response)
41-
response.status >= 400 && response.headers.empty?
42-
end
43-
44-
# Replace an unhandled error response with its configured error document.
45-
# @parameter request [Utopia::Request] The request.
46-
# @parameter response [Protocol::HTTP::Response] The unhandled error response.
47-
# @parameter location [String] The configured error document path.
48-
# @returns [Protocol::HTTP::Response] The error-document response.
49-
# @raises [RequestFailure] If the configured error document also fails.
50-
def replace_error(request, response, location)
51-
resource_status = response.status
52-
53-
# The original response is replaced by the configured error document:
54-
response.close
55-
56-
error_request = request.with(method: "GET", path_info: location)
57-
error_response = Response.wrap(@delegate.call(error_request))
58-
59-
if error_response.status >= 400
60-
error = RequestFailure.new(request.path_info, resource_status, location, error_response.status)
61-
62-
# The failed error document will not be returned to the server:
63-
error_response.close(error)
64-
65-
raise error
66-
end
67-
68-
# Feed the error code back with the error document:
69-
error_response.status = resource_status
70-
return error_response
71-
end
72-
73-
# Apply request redirections, invoke the delegate, and handle error responses.
36+
# Apply request redirections and invoke the delegate when none match.
7437
# @parameter request [Utopia::Request] The request.
7538
# @returns [Protocol::HTTP::Response] The resulting response.
7639
def call(request)
@@ -83,13 +46,7 @@ def call(request)
8346
end
8447
end
8548

86-
response = Response.wrap(@delegate.call(request))
87-
88-
if unhandled_error?(response) && location = @errors[response.status]
89-
return replace_error(request, response, location)
90-
end
91-
92-
return response
49+
return @delegate.call(request)
9350
end
9451
end
9552
end

releases.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
- **Security** Fix handling of redirects that start with `//` to prevent open redirect vulnerabilities.
66
- Use `protocol-media` and `protocol-http` for response and language negotiation, removing the `http-accept` dependency.
7-
- Combine request redirections and error documents into one configurable redirection middleware.
7+
- Combine client-facing request redirections into one configurable middleware.
88

99
## v2.31.0
1010

setup/site/config/application.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
use Utopia::Redirection do |redirects|
2929
redirects.rewrite "/" => "/welcome/index"
3030
redirects.directory_index
31-
redirects.error 404, "/errors/file-not-found"
3231
end
32+
use Utopia::Redirection::Errors, 404 => "/errors/file-not-found"
3333

3434
use Utopia::Session,
3535
expires_after: 3600 * 24,

test/utopia/.performance/config/application.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
use Utopia::Redirection do |redirects|
1515
redirects.rewrite "/" => "/welcome/index"
1616
redirects.directory_index
17-
redirects.error 404, "/errors/file-not-found"
1817
end
18+
use Utopia::Redirection::Errors, 404 => "/errors/file-not-found"
1919

2020
use Utopia::Controller, root: ROOT
2121
use Utopia::Static, root: ROOT

test/utopia/controller/respond.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,7 @@ def mock_request(path, headers = {})
154154
root = File.expand_path(".respond", __dir__)
155155

156156
Utopia::Application.build(Protocol::HTTP::Middleware.for{|request| Utopia::Response[404, {}, []]}) do
157-
use Utopia::Redirection do |redirects|
158-
redirects.error 404, "/fail"
159-
end
157+
use Utopia::Redirection::Errors, 404 => "/fail"
160158
use Utopia::Controller, root: root
161159
use Utopia::Content, root: root
162160
end

0 commit comments

Comments
 (0)