Skip to content

Commit 3af34f9

Browse files
feat: make webhook headers case insensitive (#130)
1 parent 26f3b4b commit 3af34f9

File tree

4 files changed

+31
-23
lines changed

4 files changed

+31
-23
lines changed

src/finch/_utils/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from ._utils import is_required_type as is_required_type
2323
from ._utils import is_annotated_type as is_annotated_type
2424
from ._utils import maybe_coerce_float as maybe_coerce_float
25+
from ._utils import get_required_header as get_required_header
2526
from ._utils import maybe_coerce_boolean as maybe_coerce_boolean
2627
from ._utils import maybe_coerce_integer as maybe_coerce_integer
2728
from ._utils import strip_annotated_type as strip_annotated_type

src/finch/_utils/_utils.py

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
from __future__ import annotations
22

33
import os
4+
import re
45
import inspect
56
import functools
67
from typing import Any, Mapping, TypeVar, Callable, Iterable, Sequence, cast, overload
78
from pathlib import Path
89
from typing_extensions import Required, Annotated, TypeGuard, get_args, get_origin
910

10-
from .._types import NotGiven, FileTypes, NotGivenOr
11+
from .._types import Headers, NotGiven, FileTypes, NotGivenOr, HeadersLike
1112
from .._compat import is_union as _is_union
1213
from .._compat import parse_date as parse_date
1314
from .._compat import parse_datetime as parse_datetime
@@ -351,3 +352,22 @@ def file_from_path(path: str) -> FileTypes:
351352
contents = Path(path).read_bytes()
352353
file_name = os.path.basename(path)
353354
return (file_name, contents)
355+
356+
357+
def get_required_header(headers: HeadersLike, header: str) -> str:
358+
lower_header = header.lower()
359+
if isinstance(headers, Mapping):
360+
headers = cast(Headers, headers)
361+
for k, v in headers.items():
362+
if k.lower() == lower_header and isinstance(v, str):
363+
return v
364+
365+
""" to deal with the case where the header looks like Finch-Event-Id """
366+
intercaps_header = re.sub(r"([^\w])(\w)", lambda pat: pat.group(1) + pat.group(2).upper(), header.capitalize())
367+
368+
for normalized_header in [header, lower_header, header.upper(), intercaps_header]:
369+
value = headers.get(normalized_header)
370+
if value:
371+
return value
372+
373+
raise ValueError(f"Could not find {header} header")

src/finch/resources/webhooks.py

+7-20
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from datetime import datetime, timezone, timedelta
1111

1212
from .._types import HeadersLike
13+
from .._utils import get_required_header
1314
from .._resource import SyncAPIResource, AsyncAPIResource
1415

1516
__all__ = ["Webhooks", "AsyncWebhooks"]
@@ -51,13 +52,8 @@ def verify_signature(
5152
except Exception:
5253
raise ValueError("Bad secret")
5354

54-
msg_id = headers.get("finch-event-id")
55-
if not msg_id:
56-
raise ValueError("Could not find finch-event-id header")
57-
58-
msg_timestamp = headers.get("finch-timestamp")
59-
if not msg_timestamp:
60-
raise ValueError("Could not find finch-timestamp header")
55+
msg_id = get_required_header(headers, "finch-event-id")
56+
msg_timestamp = get_required_header(headers, "finch-timestamp")
6157

6258
# validate the timestamp
6359
webhook_tolerance = timedelta(minutes=5)
@@ -88,9 +84,7 @@ def verify_signature(
8884
to_sign = f"{msg_id}.{timestamp_str}.{body}".encode()
8985
expected_signature = hmac.new(parsedSecret, to_sign, hashlib.sha256).digest()
9086

91-
msg_signature = headers.get("finch-signature")
92-
if not msg_signature:
93-
raise ValueError("Could not find finch-signature header")
87+
msg_signature = get_required_header(headers, "finch-signature")
9488

9589
# Signature header can contain multiple signatures delimited by spaces
9690
passed_sigs = msg_signature.split(" ")
@@ -151,13 +145,8 @@ def verify_signature(
151145
except Exception:
152146
raise ValueError("Bad secret")
153147

154-
msg_id = headers.get("finch-event-id")
155-
if not msg_id:
156-
raise ValueError("Could not find finch-event-id header")
157-
158-
msg_timestamp = headers.get("finch-timestamp")
159-
if not msg_timestamp:
160-
raise ValueError("Could not find finch-timestamp header")
148+
msg_id = get_required_header(headers, "finch-event-id")
149+
msg_timestamp = get_required_header(headers, "finch-timestamp")
161150

162151
# validate the timestamp
163152
webhook_tolerance = timedelta(minutes=5)
@@ -188,9 +177,7 @@ def verify_signature(
188177
to_sign = f"{msg_id}.{timestamp_str}.{body}".encode()
189178
expected_signature = hmac.new(parsedSecret, to_sign, hashlib.sha256).digest()
190179

191-
msg_signature = headers.get("finch-signature")
192-
if not msg_signature:
193-
raise ValueError("Could not find finch-signature header")
180+
msg_signature = get_required_header(headers, "finch-signature")
194181

195182
# Signature header can contain multiple signatures delimited by spaces
196183
passed_sigs = msg_signature.split(" ")

tests/api_resources/test_webhooks.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class TestWebhooks:
2727
payload = """{"company_id":"720be419-0293-4d32-a707-32179b0827ab"}"""
2828
signature = "m7y0TV2C+hlHxU42wCieApTSTaA8/047OAplBqxIV/s="
2929
headers = {
30-
"finch-event-id": "msg_2Lh9KRb0pzN4LePd3XiA4v12Axj",
30+
"Finch-Event-Id": "msg_2Lh9KRb0pzN4LePd3XiA4v12Axj",
3131
"finch-timestamp": timestamp,
3232
"finch-signature": f"v1,{signature}",
3333
}
@@ -127,7 +127,7 @@ class TestAsyncWebhooks:
127127
payload = """{"company_id":"720be419-0293-4d32-a707-32179b0827ab"}"""
128128
signature = "m7y0TV2C+hlHxU42wCieApTSTaA8/047OAplBqxIV/s="
129129
headers = {
130-
"finch-event-id": "msg_2Lh9KRb0pzN4LePd3XiA4v12Axj",
130+
"Finch-Event-Id": "msg_2Lh9KRb0pzN4LePd3XiA4v12Axj",
131131
"finch-timestamp": timestamp,
132132
"finch-signature": f"v1,{signature}",
133133
}

0 commit comments

Comments
 (0)