Skip to content

Commit 34c98fd

Browse files
lizkenyonclaude
andcommitted
Support new shopify-* webhook header format
Update Webhooks::Request to accept both the new `shopify-*` header format (e.g., `shopify-hmac-sha256`) and the legacy `x-shopify-*` format, with the new format taking precedence when both are present. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f980d8d commit 34c98fd

3 files changed

Lines changed: 107 additions & 10 deletions

File tree

lib/shopify_api/webhooks/request.rb

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,27 @@ class Request
99

1010
sig { override.returns(String) }
1111
def hmac
12-
Digest.hexencode(Base64.decode64(T.cast(@headers["x-shopify-hmac-sha256"], String)))
12+
Digest.hexencode(Base64.decode64(T.cast(shopify_header("hmac-sha256"), String)))
1313
end
1414

1515
sig { returns(String) }
1616
def topic
17-
T.cast(@headers["x-shopify-topic"], String)
17+
T.cast(shopify_header("topic"), String)
1818
end
1919

2020
sig { returns(String) }
2121
def shop
22-
T.cast(@headers["x-shopify-shop-domain"], String)
22+
T.cast(shopify_header("shop-domain"), String)
2323
end
2424

2525
sig { returns(String) }
2626
def api_version
27-
T.cast(@headers["x-shopify-api-version"], String)
27+
T.cast(shopify_header("api-version"), String)
2828
end
2929

3030
sig { returns(String) }
3131
def webhook_id
32-
T.cast(@headers["x-shopify-webhook-id"], String)
32+
T.cast(shopify_header("webhook-id"), String)
3333
end
3434

3535
sig { override.returns(String) }
@@ -48,11 +48,11 @@ def initialize(raw_body:, headers:)
4848
headers = headers.to_h { |k, v| [k.to_s.downcase.sub("http_", "").gsub("_", "-"), v] }
4949

5050
missing_headers = []
51-
[
52-
"x-shopify-topic",
53-
"x-shopify-hmac-sha256",
54-
"x-shopify-shop-domain",
55-
].each { |header| missing_headers << header unless headers.include?(header) }
51+
["topic", "hmac-sha256", "shop-domain"].each do |name|
52+
unless headers.key?("shopify-#{name}") || headers.key?("x-shopify-#{name}")
53+
missing_headers << "shopify-#{name} or x-shopify-#{name}"
54+
end
55+
end
5656
unless missing_headers.empty?
5757
raise Errors::InvalidWebhookError,
5858
"Missing one or more of the required HTTP headers to process webhooks: #{missing_headers}"
@@ -61,6 +61,13 @@ def initialize(raw_body:, headers:)
6161
@headers = headers
6262
@raw_body = raw_body
6363
end
64+
65+
private
66+
67+
sig { params(name: String).returns(T.untyped) }
68+
def shopify_header(name)
69+
@headers["shopify-#{name}"] || @headers["x-shopify-#{name}"]
70+
end
6471
end
6572
end
6673
end

test/webhooks/registry_test.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,44 @@ def test_process_with_response_as_struct
263263
assert(handler_called)
264264
end
265265

266+
def test_process_with_new_format_headers
267+
handler_called = false
268+
269+
handler = TestHelpers::FakeWebhookHandler.new(
270+
lambda do |data|
271+
assert_equal(@topic, data.topic)
272+
assert_equal(@shop, data.shop)
273+
assert_equal({}, data.body)
274+
assert_equal("b1234-eefd-4c9e-9520-049845a02082", data.webhook_id)
275+
assert_equal("2024-01", data.api_version)
276+
handler_called = true
277+
end,
278+
)
279+
280+
ShopifyAPI::Webhooks::Registry.add_registration(
281+
topic: @topic, path: "path", delivery_method: :http, handler: handler,
282+
)
283+
284+
hmac = OpenSSL::HMAC.digest(
285+
OpenSSL::Digest.new("sha256"),
286+
ShopifyAPI::Context.api_secret_key,
287+
"{}",
288+
)
289+
290+
new_format_headers = {
291+
"shopify-topic" => @topic,
292+
"shopify-hmac-sha256" => Base64.encode64(hmac),
293+
"shopify-shop-domain" => @shop,
294+
"shopify-webhook-id" => "b1234-eefd-4c9e-9520-049845a02082",
295+
"shopify-api-version" => "2024-01",
296+
}
297+
298+
webhook_request = ShopifyAPI::Webhooks::Request.new(raw_body: "{}", headers: new_format_headers)
299+
ShopifyAPI::Webhooks::Registry.process(webhook_request)
300+
301+
assert(handler_called)
302+
end
303+
266304
def test_process_hmac_validation_fails
267305
headers = {
268306
"x-shopify-topic" => "some/topic",

test/webhooks/request_test.rb

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,58 @@ def test_with_symbol_headers
3232

3333
assert(ShopifyAPI::Webhooks::Request.new(raw_body: "{}", headers: headers))
3434
end
35+
36+
def test_create_webhook_request_with_new_format_headers
37+
headers = {
38+
"shopify-topic" => "some/topic",
39+
"shopify-hmac-sha256" => "some_hmac",
40+
"shopify-shop-domain" => "shop.myshopify.com",
41+
}
42+
43+
assert(ShopifyAPI::Webhooks::Request.new(raw_body: "{}", headers: headers))
44+
end
45+
46+
def test_create_webhook_request_with_both_header_formats
47+
headers = {
48+
"x-shopify-topic" => "some/topic",
49+
"x-shopify-hmac-sha256" => "some_hmac",
50+
"x-shopify-shop-domain" => "shop.myshopify.com",
51+
"shopify-topic" => "some/topic",
52+
"shopify-hmac-sha256" => "some_hmac",
53+
"shopify-shop-domain" => "shop.myshopify.com",
54+
}
55+
56+
assert(ShopifyAPI::Webhooks::Request.new(raw_body: "{}", headers: headers))
57+
end
58+
59+
def test_accessor_values_with_new_format_headers
60+
hmac_value = Base64.encode64("test_hmac_bytes")
61+
headers = {
62+
"shopify-topic" => "orders/create",
63+
"shopify-hmac-sha256" => hmac_value,
64+
"shopify-shop-domain" => "test-shop.myshopify.com",
65+
"shopify-api-version" => "2024-01",
66+
"shopify-webhook-id" => "b1234-eefd-4c9e-9520-049845a02082",
67+
}
68+
69+
request = ShopifyAPI::Webhooks::Request.new(raw_body: "{}", headers: headers)
70+
71+
assert_equal("orders/create", request.topic)
72+
assert_equal("test-shop.myshopify.com", request.shop)
73+
assert_equal("2024-01", request.api_version)
74+
assert_equal("b1234-eefd-4c9e-9520-049845a02082", request.webhook_id)
75+
assert_equal(Digest.hexencode(Base64.decode64(hmac_value)), request.hmac)
76+
end
77+
78+
def test_error_when_headers_missing_in_either_format
79+
error = assert_raises(ShopifyAPI::Errors::InvalidWebhookError) do
80+
ShopifyAPI::Webhooks::Request.new(raw_body: "{}", headers: {})
81+
end
82+
83+
assert_includes(error.message, "shopify-topic or x-shopify-topic")
84+
assert_includes(error.message, "shopify-hmac-sha256 or x-shopify-hmac-sha256")
85+
assert_includes(error.message, "shopify-shop-domain or x-shopify-shop-domain")
86+
end
3587
end
3688
end
3789
end

0 commit comments

Comments
 (0)