Skip to content

Commit 43a083c

Browse files
Expose full problem document on errors (#268)
Co-authored-by: Nick Silverman <nckslvrmn@gmail.com>
1 parent c5f1e27 commit 43a083c

File tree

4 files changed

+104
-5
lines changed

4 files changed

+104
-5
lines changed

lib/acme/client/error.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
class Acme::Client::Error < StandardError
22
attr_reader :subproblems
3+
attr_reader :acme_error_body
4+
35

46
Subproblem = Struct.new(:type, :detail, :identifier, keyword_init: true) do
57
def to_h
68
{ type: type, detail: detail, identifier: identifier }
79
end
810
end
911

10-
def initialize(message = nil, subproblems: nil)
12+
def initialize(message = nil, acme_error_body: nil, subproblems: nil)
1113
super(message)
14+
@acme_error_body = acme_error_body
1215
@subproblems = parse_subproblems(subproblems)
1316
end
1417

lib/acme/client/error/rate_limited.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ class Acme::Client::Error::RateLimited < Acme::Client::Error::ServerError
33

44
DEFAULT_MESSAGE = 'Error message: urn:ietf:params:acme:error:rateLimited'
55

6-
def initialize(message = DEFAULT_MESSAGE, retry_after = 10, subproblems: nil)
7-
super(message, subproblems: subproblems)
6+
def initialize(message = DEFAULT_MESSAGE, retry_after = 10, acme_error_body: nil, subproblems: nil)
7+
super(message, acme_error_body: acme_error_body, subproblems: subproblems)
88
@retry_after = retry_after.nil? ? 10 : retry_after.to_i
99
end
1010
end

lib/acme/client/http_client.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,12 @@ def raise_on_not_found!
101101
end
102102

103103
def raise_on_error!
104+
body = env.body.is_a?(Hash) ? env.body : nil
104105
subproblems = error_subproblems
105106
if error_class == Acme::Client::Error::RateLimited
106-
raise error_class.new(error_message, env.response_headers['Retry-After'], subproblems: subproblems)
107+
raise error_class.new(error_message, env.response_headers['Retry-After'], acme_error_body: body, subproblems: subproblems)
107108
end
108-
raise error_class.new(error_message, subproblems: subproblems)
109+
raise error_class.new(error_message, acme_error_body: body, subproblems: subproblems)
109110
end
110111

111112
def error_message

spec/acme_error_body_spec.rb

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# frozen_string_literal: true
2+
3+
$LOAD_PATH.unshift File.join(__dir__, '../lib')
4+
5+
require 'acme/client'
6+
7+
RSpec.describe 'Full problem document (acme_error_body) support' do
8+
let(:problem_document) do
9+
{
10+
'type' => 'urn:ietf:params:acme:error:rateLimited',
11+
'detail' => 'too many certificates already issued for "example.com"',
12+
'status' => 429,
13+
'instance' => 'https://ca.example.com/acme/error/abc123'
14+
}
15+
end
16+
17+
describe Acme::Client::Error do
18+
it 'exposes the full problem document when provided' do
19+
error = Acme::Client::Error.new('rate limited', acme_error_body: problem_document)
20+
expect(error.acme_error_body).to eq(problem_document)
21+
end
22+
23+
it 'defaults to nil when not provided' do
24+
error = Acme::Client::Error.new('some error')
25+
expect(error.acme_error_body).to be_nil
26+
end
27+
28+
it 'works with no arguments' do
29+
error = Acme::Client::Error.new
30+
expect(error.acme_error_body).to be_nil
31+
end
32+
33+
it 'preserves CA-specific extension fields' do
34+
body = problem_document.merge('boulder-requester' => '12345')
35+
error = Acme::Client::Error.new('error', acme_error_body: body)
36+
expect(error.acme_error_body['boulder-requester']).to eq('12345')
37+
end
38+
39+
it 'provides access to status from the problem document' do
40+
error = Acme::Client::Error.new('error', acme_error_body: problem_document)
41+
expect(error.acme_error_body['status']).to eq(429)
42+
end
43+
44+
it 'provides access to instance URL from the problem document' do
45+
error = Acme::Client::Error.new('error', acme_error_body: problem_document)
46+
expect(error.acme_error_body['instance']).to eq('https://ca.example.com/acme/error/abc123')
47+
end
48+
end
49+
50+
describe 'acme_error_body on error subclasses' do
51+
it 'works on ServerError subclasses' do
52+
error = Acme::Client::Error::Unauthorized.new('unauthorized', acme_error_body: problem_document)
53+
expect(error.acme_error_body).to eq(problem_document)
54+
end
55+
56+
it 'works on RateLimited with positional args preserved' do
57+
error = Acme::Client::Error::RateLimited.new('rate limited', 60, acme_error_body: problem_document)
58+
expect(error.retry_after).to eq(60)
59+
expect(error.acme_error_body).to eq(problem_document)
60+
end
61+
62+
it 'works on RateLimited with defaults' do
63+
error = Acme::Client::Error::RateLimited.new
64+
expect(error.retry_after).to eq(10)
65+
expect(error.acme_error_body).to be_nil
66+
end
67+
68+
it 'all ACME_ERRORS subclasses accept acme_error_body keyword' do
69+
Acme::Client::Error::ACME_ERRORS.each_value do |error_class|
70+
next if error_class == Acme::Client::Error::RateLimited
71+
72+
error = error_class.new('test', acme_error_body: problem_document)
73+
expect(error.acme_error_body).to eq(problem_document),
74+
"#{error_class} did not accept acme_error_body correctly"
75+
end
76+
end
77+
end
78+
79+
describe 'problem documents with subproblems field' do
80+
it 'preserves subproblems in the raw body for downstream parsing' do
81+
body = problem_document.merge(
82+
'subproblems' => [
83+
{
84+
'type' => 'urn:ietf:params:acme:error:rejectedIdentifier',
85+
'detail' => 'CA will not issue for "example.net"',
86+
'identifier' => { 'type' => 'dns', 'value' => 'example.net' }
87+
}
88+
]
89+
)
90+
error = Acme::Client::Error::Malformed.new('rejected', acme_error_body: body)
91+
expect(error.acme_error_body['subproblems'].length).to eq(1)
92+
expect(error.acme_error_body['subproblems'].first['identifier']['value']).to eq('example.net')
93+
end
94+
end
95+
end

0 commit comments

Comments
 (0)