Skip to content

Commit 4701231

Browse files
committed
harden feed responder and resolver
1 parent 1a691ad commit 4701231

4 files changed

Lines changed: 94 additions & 11 deletions

File tree

app/feeds/responder.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ def call(request:, target_kind:, identifier:)
2727
resolved_source = SourceResolver.call(feed_request)
2828
result = Service.call(resolved_source)
2929
normalized_identifier = feed_request.feed_name || identifier
30+
body = write_response(response: request.response, representation: feed_request.representation, result:)
3031

3132
emit_result(target_kind:, identifier: normalized_identifier, resolved_source:, result:)
32-
write_response(response: request.response, representation: feed_request.representation, result:)
33+
body
3334
rescue StandardError => error
3435
emit_failure(target_kind:, identifier:, error:)
3536
raise

app/feeds/source_resolver.rb

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,13 @@ def call(feed_request)
3737
# @return [Html2rss::Web::Feeds::Contracts::ResolvedSource]
3838
def resolve_static(feed_request)
3939
config = LocalConfig.find(feed_request.feed_name)
40-
config[:params] = (config[:params] || {}).merge(feed_request.params) if feed_request.params.any?
41-
config[:strategy] ||= Html2rss::RequestService.default_strategy_name
40+
generator_input = static_generator_input(config, feed_request.params)
4241

4342
Contracts::ResolvedSource.new(
4443
source_kind: :static,
4544
cache_identity: static_cache_identity(feed_request.feed_name, feed_request.params),
46-
generator_input: config,
47-
ttl_seconds: CacheTtl.seconds_from_minutes(config.dig(:channel, :ttl))
45+
generator_input: generator_input,
46+
ttl_seconds: CacheTtl.seconds_from_minutes(generator_input.dig(:channel, :ttl))
4847
)
4948
end
5049

@@ -73,6 +72,25 @@ def static_cache_identity(feed_name, params)
7372
"static:#{feed_name}:#{digest}"
7473
end
7574

75+
# @param config [Hash{Symbol=>Object}]
76+
# @param params [Hash{Object=>Object}]
77+
# @return [Hash{Symbol=>Object}]
78+
def static_generator_input(config, params)
79+
generator_input = config.dup
80+
generator_input[:params] = merged_static_params(config, params)
81+
generator_input[:strategy] ||= Html2rss::RequestService.default_strategy_name
82+
generator_input
83+
end
84+
85+
# @param config [Hash{Symbol=>Object}]
86+
# @param params [Hash{Object=>Object}]
87+
# @return [Hash{Object=>Object}]
88+
def merged_static_params(config, params)
89+
return (config[:params] || {}).dup if params.empty?
90+
91+
(config[:params] || {}).merge(params)
92+
end
93+
7694
# @param token [String]
7795
# @return [String]
7896
def token_cache_identity(token)

spec/html2rss/web/feeds/responder_spec.rb

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,17 @@ def resolved_source
6565

6666
expect(response['Cache-Control']).to include('public')
6767
end
68+
69+
it 'emits success after writing the response' do
70+
write_response
71+
72+
expect(Html2rss::Web::Observability).to have_received(:emit).with(
73+
event_name: 'feed.render',
74+
outcome: 'success',
75+
details: include(strategy: :ssrf_filter, url: 'https://example.com'),
76+
level: :info
77+
)
78+
end
6879
end
6980

7081
context 'with an error result' do
@@ -108,6 +119,48 @@ def resolved_source
108119
end
109120
end
110121

122+
context 'when response rendering fails after feed generation succeeds' do
123+
subject(:write_response) do
124+
described_class.call(
125+
request: request,
126+
target_kind: :token,
127+
identifier: 'token'
128+
)
129+
end
130+
131+
let(:representation) { Html2rss::Web::FeedResponseFormat::RSS }
132+
133+
let(:result) do
134+
Html2rss::Web::Feeds::Contracts::RenderResult.new(
135+
status: :ok,
136+
payload: nil,
137+
message: nil,
138+
ttl_seconds: 600,
139+
cache_key: 'feed_result:test',
140+
error_message: nil
141+
)
142+
end
143+
144+
before do
145+
allow(Html2rss::Web::Feeds::Request).to receive(:call).and_return(feed_request(representation))
146+
allow(Html2rss::Web::Feeds::SourceResolver).to receive(:call).and_return(resolved_source)
147+
allow(Html2rss::Web::Feeds::Service).to receive(:call).and_return(result)
148+
allow(Html2rss::Web::Observability).to receive(:emit)
149+
allow(Html2rss::Web::Feeds::RssRenderer).to receive(:call).and_raise(StandardError, 'render failed')
150+
end
151+
152+
it 'emits only the failure event' do
153+
expect { write_response }.to raise_error(StandardError, 'render failed')
154+
155+
expect(Html2rss::Web::Observability).to have_received(:emit).once.with(
156+
event_name: 'feed.render',
157+
outcome: 'failure',
158+
details: include(error_class: 'StandardError', error_message: 'render failed'),
159+
level: :warn
160+
)
161+
end
162+
end
163+
111164
private
112165

113166
# @param body [String]

spec/html2rss/web/feeds/source_resolver_spec.rb

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ def resolved_tuple(resolved)
1010
end
1111

1212
context 'with a static request' do
13+
let(:config) do
14+
{
15+
channel: { ttl: 15, url: 'https://example.com/feed' },
16+
params: { 'existing' => '1' }
17+
}
18+
end
19+
1320
let(:feed_request) do
1421
Html2rss::Web::Feeds::Contracts::Request.new(
1522
target_kind: :static,
@@ -21,12 +28,7 @@ def resolved_tuple(resolved)
2128
end
2229

2330
before do
24-
allow(Html2rss::Web::LocalConfig).to receive(:find).with('legacy').and_return(
25-
{
26-
channel: { ttl: 15, url: 'https://example.com/feed' },
27-
params: { 'existing' => '1' }
28-
}
29-
)
31+
allow(Html2rss::Web::LocalConfig).to receive(:find).with('legacy').and_return(config)
3032
allow(Html2rss::RequestService).to receive(:default_strategy_name).and_return(:ssrf_filter)
3133
end
3234

@@ -42,6 +44,15 @@ def resolved_tuple(resolved)
4244
]
4345
)
4446
end
47+
48+
it 'does not mutate the source config hash' do
49+
described_class.call(feed_request)
50+
51+
expect(config).to eq(
52+
channel: { ttl: 15, url: 'https://example.com/feed' },
53+
params: { 'existing' => '1' }
54+
)
55+
end
4556
end
4657

4758
context 'with a token request' do

0 commit comments

Comments
 (0)