Skip to content

Commit e20fc81

Browse files
CopilotGrantBirki
andcommitted
Remove symbolization options and update to pure JSON format
Co-authored-by: GrantBirki <[email protected]>
1 parent cf89b19 commit e20fc81

File tree

9 files changed

+51
-92
lines changed

9 files changed

+51
-92
lines changed

docs/handler_plugins.md

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ Handler plugins are Ruby classes that extend the `Hooks::Plugins::Handlers::Base
1515
class Example < Hooks::Plugins::Handlers::Base
1616
# Process a webhook payload
1717
#
18-
# @param payload [Hash, String] webhook payload (symbolized keys by default)
19-
# @param headers [Hash] HTTP headers (symbolized keys by default)
18+
# @param payload [Hash, String] webhook payload (pure JSON with string keys)
19+
# @param headers [Hash] HTTP headers (string keys, optionally normalized)
2020
# @param config [Hash] Endpoint configuration
2121
# @return [Hash] Response data
2222
def call(payload:, headers:, config:)
@@ -31,9 +31,9 @@ end
3131

3232
The `payload` parameter can be a Hash or a String. If the payload is a String, it will be parsed as JSON. If it is a Hash, it will be passed directly to the handler. The payload can contain any data that the webhook sender wants to send.
3333

34-
By default, the payload is parsed as JSON (if it can be) and then symbolized. This means that the keys in the payload will be converted to symbols. You can disable this auto-symbolization of the payload by setting the environment variable `HOOKS_SYMBOLIZE_PAYLOAD` to `false` or by setting the `symbolize_payload` option to `false` in the global configuration file.
34+
The payload is parsed as JSON (if it can be) and returned as a pure Ruby hash with string keys, maintaining the original JSON structure. This ensures that the payload is always a valid JSON representation that can be easily serialized and processed.
3535

36-
**TL;DR**: The payload is almost always a Hash with symbolized keys, regardless of whether the original payload was a Hash or a JSON String.
36+
**TL;DR**: The payload is almost always a Hash with string keys, regardless of whether the original payload was a Hash or a JSON String.
3737

3838
For example, if the client sends the following JSON payload:
3939

@@ -50,20 +50,20 @@ It will be parsed and passed to the handler as:
5050

5151
```ruby
5252
{
53-
hello: "world",
54-
foo: ["bar", "baz"],
55-
truthy: true,
56-
coffee: {is: "good"}
53+
"hello" => "world",
54+
"foo" => ["bar", "baz"],
55+
"truthy" => true,
56+
"coffee" => {"is" => "good"}
5757
}
5858
```
5959

6060
### `headers` Parameter
6161

6262
The `headers` parameter is a Hash that contains the HTTP headers that were sent with the webhook request. It includes standard headers like `host`, `user-agent`, `accept`, and any custom headers that the webhook sender may have included.
6363

64-
By default, the headers are normalized (lowercased and trimmed) and then symbolized. This means that the keys in the headers will be converted to symbols, and any hyphens (`-`) in header names are converted to underscores (`_`). You can disable header symbolization by setting the environment variable `HOOKS_SYMBOLIZE_HEADERS` to `false` or by setting the `symbolize_headers` option to `false` in the global configuration file.
64+
By default, the headers are normalized (lowercased and trimmed) but kept as string keys to maintain their JSON representation. Header keys are always strings, and any normalization simply ensures consistent formatting (lowercasing and trimming whitespace). You can disable header normalization by setting the environment variable `HOOKS_NORMALIZE_HEADERS` to `false` or by setting the `normalize_headers` option to `false` in the global configuration file.
6565

66-
**TL;DR**: The headers are almost always a Hash with symbolized keys, with hyphens converted to underscores.
66+
**TL;DR**: The headers are always a Hash with string keys, optionally normalized for consistency.
6767

6868
For example, if the client sends the following headers:
6969

@@ -79,25 +79,23 @@ X-Forwarded-Proto: https
7979
Authorization: Bearer <TOKEN>
8080
```
8181

82-
They will be normalized and symbolized and passed to the handler as:
82+
They will be normalized and passed to the handler as:
8383

8484
```ruby
8585
{
86-
host: "hooks.example.com",
87-
user_agent: "foo-client/1.0",
88-
accept: "application/json, text/plain, */*",
89-
accept_encoding: "gzip, compress, deflate, br",
90-
client_name: "foo",
91-
x_forwarded_for: "<IP_ADDRESS>",
92-
x_forwarded_host: "hooks.example.com",
93-
x_forwarded_proto: "https",
94-
authorization: "Bearer <TOKEN>" # a careful reminder that headers *can* contain sensitive information!
86+
"host" => "hooks.example.com",
87+
"user-agent" => "foo-client/1.0",
88+
"accept" => "application/json, text/plain, */*",
89+
"accept-encoding" => "gzip, compress, deflate, br",
90+
"client-name" => "foo",
91+
"x-forwarded-for" => "<IP_ADDRESS>",
92+
"x-forwarded-host" => "hooks.example.com",
93+
"x-forwarded-proto" => "https",
94+
"authorization" => "Bearer <TOKEN>" # a careful reminder that headers *can* contain sensitive information!
9595
}
9696
```
9797

98-
It should be noted that the `headers` parameter is a Hash with **symbolized keys** (not strings) by default. They are also normalized (lowercased and trimmed) to ensure consistency.
99-
100-
You can disable header symbolization by either setting the environment variable `HOOKS_SYMBOLIZE_HEADERS` to `false` or by setting the `symbolize_headers` option to `false` in the global configuration file.
98+
It should be noted that the `headers` parameter is a Hash with **string keys** (not symbols). They are optionally normalized (lowercased and trimmed) to ensure consistency.
10199

102100
You can disable header normalization by either setting the environment variable `HOOKS_NORMALIZE_HEADERS` to `false` or by setting the `normalize_headers` option to `false` in the global configuration file.
103101

lib/hooks/app/api.rb

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,14 +102,13 @@ def self.create(config:, endpoints:, log:)
102102
validate_auth!(raw_body, headers, endpoint_config, config)
103103
end
104104

105-
payload = parse_payload(raw_body, headers, symbolize: config[:symbolize_payload])
105+
payload = parse_payload(raw_body, headers, symbolize: false)
106106
handler = load_handler(handler_class_name)
107-
normalized_headers = config[:normalize_headers] ? Hooks::Utils::Normalize.headers(headers) : headers
108-
symbolized_headers = config[:symbolize_headers] ? Hooks::Utils::Normalize.symbolize_headers(normalized_headers) : normalized_headers
107+
processed_headers = config[:normalize_headers] ? Hooks::Utils::Normalize.headers(headers) : headers
109108

110109
response = handler.call(
111110
payload:,
112-
headers: symbolized_headers,
111+
headers: processed_headers,
113112
config: endpoint_config
114113
)
115114

lib/hooks/app/helpers.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ def enforce_request_limits(config)
4444
#
4545
# @param raw_body [String] The raw request body
4646
# @param headers [Hash] The request headers
47-
# @param symbolize [Boolean] Whether to symbolize keys in parsed JSON (default: true)
48-
# @return [Hash, String] Parsed JSON as Hash (optionally symbolized), or raw body if not JSON
49-
def parse_payload(raw_body, headers, symbolize: true)
47+
# @param symbolize [Boolean] Whether to symbolize keys in parsed JSON (default: false)
48+
# @return [Hash, String] Parsed JSON as Hash with string keys, or raw body if not JSON
49+
def parse_payload(raw_body, headers, symbolize: false)
5050
# Optimized content type check - check most common header first
5151
content_type = headers["Content-Type"] || headers["CONTENT_TYPE"] || headers["content-type"] || headers["HTTP_CONTENT_TYPE"]
5252

@@ -55,6 +55,7 @@ def parse_payload(raw_body, headers, symbolize: true)
5555
begin
5656
# Security: Limit JSON parsing depth and complexity to prevent JSON bombs
5757
parsed_payload = safe_json_parse(raw_body)
58+
# Note: symbolize parameter is kept for backward compatibility but defaults to false
5859
parsed_payload = parsed_payload.transform_keys(&:to_sym) if symbolize && parsed_payload.is_a?(Hash)
5960
return parsed_payload
6061
rescue JSON::ParserError, ArgumentError => e

lib/hooks/core/config_loader.rb

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ class ConfigLoader
2020
production: true,
2121
endpoints_dir: "./config/endpoints",
2222
use_catchall_route: false,
23-
symbolize_payload: true,
24-
normalize_headers: true,
25-
symbolize_headers: true
23+
normalize_headers: true
2624
}.freeze
2725

2826
SILENCE_CONFIG_LOADER_MESSAGES = ENV.fetch(
@@ -142,9 +140,7 @@ def self.load_env_config
142140
"HOOKS_ENVIRONMENT" => :environment,
143141
"HOOKS_ENDPOINTS_DIR" => :endpoints_dir,
144142
"HOOKS_USE_CATCHALL_ROUTE" => :use_catchall_route,
145-
"HOOKS_SYMBOLIZE_PAYLOAD" => :symbolize_payload,
146143
"HOOKS_NORMALIZE_HEADERS" => :normalize_headers,
147-
"HOOKS_SYMBOLIZE_HEADERS" => :symbolize_headers,
148144
"HOOKS_SOME_STRING_VAR" => :some_string_var # Added for test
149145
}
150146

@@ -156,7 +152,7 @@ def self.load_env_config
156152
case config_key
157153
when :request_limit, :request_timeout
158154
env_config[config_key] = value.to_i
159-
when :use_catchall_route, :symbolize_payload, :normalize_headers, :symbolize_headers
155+
when :use_catchall_route, :normalize_headers
160156
# Convert string to boolean
161157
env_config[config_key] = %w[true 1 yes on].include?(value.downcase)
162158
else

lib/hooks/core/config_validator.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ class ValidationError < StandardError; end
2626
optional(:environment).filled(:string, included_in?: %w[development production])
2727
optional(:endpoints_dir).filled(:string)
2828
optional(:use_catchall_route).filled(:bool)
29-
optional(:symbolize_payload).filled(:bool)
3029
optional(:normalize_headers).filled(:bool)
3130
end
3231

spec/integration/header_symbolization_spec.rb

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44

55
require_relative "../../lib/hooks"
66

7-
describe "Header Symbolization Integration" do
7+
describe "Header Normalization Integration" do
88
let(:config) do
99
{
10-
symbolize_headers: true,
1110
normalize_headers: true
1211
}
1312
end
@@ -21,33 +20,11 @@
2120
}
2221
end
2322

24-
context "when symbolize_headers is enabled (default)" do
25-
it "normalizes and symbolizes headers" do
26-
normalized_headers = config[:normalize_headers] ? Hooks::Utils::Normalize.headers(headers) : headers
27-
symbolized_headers = config[:symbolize_headers] ? Hooks::Utils::Normalize.symbolize_headers(normalized_headers) : normalized_headers
23+
context "when normalize_headers is enabled (default)" do
24+
it "normalizes headers but keeps string keys" do
25+
processed_headers = config[:normalize_headers] ? Hooks::Utils::Normalize.headers(headers) : headers
2826

29-
expect(symbolized_headers).to eq({
30-
content_type: "application/json",
31-
x_github_event: "push",
32-
user_agent: "test-agent",
33-
accept_encoding: "gzip, br"
34-
})
35-
end
36-
end
37-
38-
context "when symbolize_headers is disabled" do
39-
let(:config) do
40-
{
41-
symbolize_headers: false,
42-
normalize_headers: true
43-
}
44-
end
45-
46-
it "normalizes but does not symbolize headers" do
47-
normalized_headers = config[:normalize_headers] ? Hooks::Utils::Normalize.headers(headers) : headers
48-
symbolized_headers = config[:symbolize_headers] ? Hooks::Utils::Normalize.symbolize_headers(normalized_headers) : normalized_headers
49-
50-
expect(symbolized_headers).to eq({
27+
expect(processed_headers).to eq({
5128
"content-type" => "application/json",
5229
"x-github-event" => "push",
5330
"user-agent" => "test-agent",
@@ -56,19 +33,17 @@
5633
end
5734
end
5835

59-
context "when both symbolize_headers and normalize_headers are disabled" do
36+
context "when normalize_headers is disabled" do
6037
let(:config) do
6138
{
62-
symbolize_headers: false,
6339
normalize_headers: false
6440
}
6541
end
6642

6743
it "passes headers through unchanged" do
68-
normalized_headers = config[:normalize_headers] ? Hooks::Utils::Normalize.headers(headers) : headers
69-
symbolized_headers = config[:symbolize_headers] ? Hooks::Utils::Normalize.symbolize_headers(normalized_headers) : normalized_headers
44+
processed_headers = config[:normalize_headers] ? Hooks::Utils::Normalize.headers(headers) : headers
7045

71-
expect(symbolized_headers).to eq(headers)
46+
expect(processed_headers).to eq(headers)
7247
end
7348
end
7449
end

spec/unit/lib/hooks/app/helpers_spec.rb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def error!(message, code)
121121

122122
result = helper.parse_payload(body, headers)
123123

124-
expect(result).to eq({ key: "value" })
124+
expect(result).to eq({ "key" => "value" })
125125
end
126126

127127
it "parses JSON that looks like JSON without content type" do
@@ -130,7 +130,7 @@ def error!(message, code)
130130

131131
result = helper.parse_payload(body, headers)
132132

133-
expect(result).to eq({ key: "value" })
133+
expect(result).to eq({ "key" => "value" })
134134
end
135135

136136
it "parses JSON arrays" do
@@ -142,15 +142,15 @@ def error!(message, code)
142142
expect(result).to eq([{ "key" => "value" }])
143143
end
144144

145-
it "symbolizes keys by default" do
145+
it "does not symbolize keys by default" do
146146
headers = { "Content-Type" => "application/json" }
147147
body = '{"string_key": "value", "nested": {"inner_key": "inner_value"}}'
148148

149149
result = helper.parse_payload(body, headers)
150150

151151
expect(result).to eq({
152-
string_key: "value",
153-
nested: { "inner_key" => "inner_value" } # Only top level is symbolized
152+
"string_key" => "value",
153+
"nested" => { "inner_key" => "inner_value" }
154154
})
155155
end
156156

@@ -171,7 +171,7 @@ def error!(message, code)
171171

172172
result = helper.parse_payload(body, headers)
173173

174-
expect(result).to eq({ key: "value" })
174+
expect(result).to eq({ "key" => "value" })
175175
end
176176

177177
it "handles lowercase content-type" do
@@ -180,7 +180,7 @@ def error!(message, code)
180180

181181
result = helper.parse_payload(body, headers)
182182

183-
expect(result).to eq({ key: "value" })
183+
expect(result).to eq({ "key" => "value" })
184184
end
185185

186186
it "handles HTTP_CONTENT_TYPE" do
@@ -189,7 +189,7 @@ def error!(message, code)
189189

190190
result = helper.parse_payload(body, headers)
191191

192-
expect(result).to eq({ key: "value" })
192+
expect(result).to eq({ "key" => "value" })
193193
end
194194
end
195195

@@ -212,7 +212,7 @@ def error!(message, code)
212212

213213
result = helper.parse_payload(nested_json, headers)
214214

215-
expect(result).to eq({ level1: { "level2" => { "level3" => { "value" => "test" } } } })
215+
expect(result).to eq({ "level1" => { "level2" => { "level3" => { "value" => "test" } } } })
216216
end
217217

218218
it "returns raw body when JSON exceeds size limits" do

spec/unit/lib/hooks/core/config_loader_spec.rb

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@
1919
production: true,
2020
endpoints_dir: "./config/endpoints",
2121
use_catchall_route: false,
22-
symbolize_payload: true,
23-
normalize_headers: true,
24-
symbolize_headers: true
22+
normalize_headers: true
2523
)
2624
end
2725
end
@@ -188,19 +186,15 @@
188186

189187
it "converts boolean environment variables correctly" do
190188
ENV["HOOKS_USE_CATCHALL_ROUTE"] = "true"
191-
ENV["HOOKS_SYMBOLIZE_PAYLOAD"] = "1"
192189
ENV["HOOKS_NORMALIZE_HEADERS"] = "yes"
193-
ENV["HOOKS_SYMBOLIZE_HEADERS"] = "on"
194190
# Add a non-boolean var to ensure it's not misinterpreted
195191
ENV["HOOKS_SOME_STRING_VAR"] = "test_value"
196192

197193

198194
config = described_class.load
199195

200196
expect(config[:use_catchall_route]).to be true
201-
expect(config[:symbolize_payload]).to be true
202197
expect(config[:normalize_headers]).to be true
203-
expect(config[:symbolize_headers]).to be true
204198
expect(config[:some_string_var]).to eq("test_value") # Check the string var
205199
end
206200
end
@@ -373,12 +367,12 @@
373367
handler: "ValidHandler"
374368
)
375369
end
376-
it "allows opt-out via environment variable" do
377-
ENV["HOOKS_SYMBOLIZE_HEADERS"] = "false"
370+
it "allows environment variable setup" do
371+
ENV["HOOKS_NORMALIZE_HEADERS"] = "false"
378372

379373
config = described_class.load
380374

381-
expect(config[:symbolize_headers]).to be false
375+
expect(config[:normalize_headers]).to be false
382376
end
383377
end
384378
end

spec/unit/lib/hooks/core/config_validator_spec.rb

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
environment: "development",
1616
endpoints_dir: "./custom/endpoints",
1717
use_catchall_route: true,
18-
symbolize_payload: false,
1918
normalize_headers: false
2019
}
2120

@@ -115,14 +114,12 @@
115114
it "coerces boolean-like string values" do
116115
config = {
117116
use_catchall_route: "true",
118-
symbolize_payload: "false",
119117
normalize_headers: "1"
120118
}
121119

122120
result = described_class.validate_global_config(config)
123121

124122
expect(result[:use_catchall_route]).to be true
125-
expect(result[:symbolize_payload]).to be false
126123
expect(result[:normalize_headers]).to be true # "1" gets coerced to true
127124
end
128125

0 commit comments

Comments
 (0)