Skip to content

Commit 52fb87f

Browse files
authored
Merge pull request #44 from github/copilot/fix-43
feat: refactor `payload` and `headers` into pure JSON
2 parents d1ed74f + a8993c2 commit 52fb87f

File tree

14 files changed

+62
-97
lines changed

14 files changed

+62
-97
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
hooks-ruby (0.0.5)
4+
hooks-ruby (0.0.6)
55
dry-schema (~> 1.14, >= 1.14.1)
66
grape (~> 2.3)
77
puma (~> 6.6)

docs/configuration.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ When set to `true`, enables a catch-all route that will handle requests to unkno
9595
**Default:** `false`
9696
**Example:** `false`
9797

98+
### `normalize_headers`
99+
100+
When set to `true`, normalizes incoming HTTP headers by lowercasing and trimming them. This ensures consistency in header names and values.
101+
102+
**Default:** `true`
103+
98104
## Endpoint Options
99105

100106
### `path`

docs/handler_plugins.md

Lines changed: 22 additions & 24 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 - default is 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,24 +50,24 @@ 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

70-
```
70+
```text
7171
Host: hooks.example.com
7272
User-Agent: foo-client/1.0
7373
Accept: application/json, text/plain, */*
@@ -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

lib/hooks/plugins/handlers/base.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class Base
1515
# Process a webhook request
1616
#
1717
# @param payload [Hash, String] Parsed request body (JSON Hash) or raw string
18-
# @param headers [Hash] HTTP headers (symbolized keys by default)
18+
# @param headers [Hash] HTTP headers (string keys, optionally normalized - default is normalized)
1919
# @param config [Hash] Merged endpoint configuration including opts section (symbolized keys)
2020
# @return [Hash, String, nil] Response body (will be auto-converted to JSON)
2121
# @raise [NotImplementedError] if not implemented by subclass

lib/hooks/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
module Hooks
55
# Current version of the Hooks webhook framework
66
# @return [String] The version string following semantic versioning
7-
VERSION = "0.0.5".freeze
7+
VERSION = "0.0.6".freeze
88
end

spec/acceptance/plugins/handlers/team1_handler.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def call(payload:, headers:, config:)
2525

2626
# Process the payload based on type
2727
if payload.is_a?(Hash)
28-
event_type = payload[:event_type] || "unknown"
28+
event_type = payload["event_type"] || "unknown"
2929

3030
case event_type
3131
when "deployment"

0 commit comments

Comments
 (0)