-
Notifications
You must be signed in to change notification settings - Fork 127
Improvement: Github webhook, CORS, global exception exception handling in FastAPI returns JSON response. #216
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,12 +1,15 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import asyncio | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import uuid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import hmac | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import hashlib | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from fastapi import APIRouter, Request, HTTPException | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import uuid | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from fastapi import APIRouter, Request, HTTPException, Header | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from app.core.events.event_bus import EventBus | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from app.core.events.enums import EventType, PlatformType | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from app.core.events.base import BaseEvent | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from app.core.handler.handler_registry import HandlerRegistry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pydantic import BaseModel | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from app.core.config.settings import settings | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| router = APIRouter() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -35,50 +38,77 @@ def register_event_handlers(): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| event_bus.register_handler(EventType.PR_COMMENTED, sample_handler, PlatformType.GITHUB) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| event_bus.register_handler(EventType.PR_MERGED, sample_handler, PlatformType.GITHUB) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def verify_signature(body: bytes, x_hub_signature_256: str = Header(None)): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """Verify that the payload was sent from GitHub by validating the SHA256 signature.""" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not settings.github_webhook_secret: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # If secret is not configured, we skip verification (not recommended for production) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not x_hub_signature_256: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise HTTPException(status_code=401, detail="X-Hub-Signature-256 header is missing") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| signature = hmac.new( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| settings.github_webhook_secret.encode(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hashlib.sha256 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ).hexdigest() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not hmac.compare_digest(f"sha256={signature}", x_hub_signature_256): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raise HTTPException(status_code=401, detail="Invalid webhook signature") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async def verify_signature(body: bytes, x_hub_signature_256: str = Header(None)): | |
| """Verify that the payload was sent from GitHub by validating the SHA256 signature.""" | |
| if not settings.github_webhook_secret: | |
| # If secret is not configured, we skip verification (not recommended for production) | |
| return | |
| if not x_hub_signature_256: | |
| raise HTTPException(status_code=401, detail="X-Hub-Signature-256 header is missing") | |
| signature = hmac.new( | |
| settings.github_webhook_secret.encode(), | |
| body, | |
| hashlib.sha256 | |
| ).hexdigest() | |
| if not hmac.compare_digest(f"sha256={signature}", x_hub_signature_256): | |
| raise HTTPException(status_code=401, detail="Invalid webhook signature") | |
| async def verify_signature(body: bytes, x_hub_signature_256: str | None): | |
| """Verify that the payload was sent from GitHub by validating the SHA256 signature.""" | |
| if not settings.github_webhook_secret: | |
| # If secret is not configured, we skip verification (not recommended for production) | |
| return | |
| if not x_hub_signature_256: | |
| raise HTTPException(status_code=401, detail="X-Hub-Signature-256 header is missing") | |
| signature = hmac.new( | |
| settings.github_webhook_secret.encode(), | |
| body, | |
| hashlib.sha256 | |
| ).hexdigest() | |
| if not hmac.compare_digest(f"sha256={signature}", x_hub_signature_256): | |
| raise HTTPException(status_code=401, detail="Invalid webhook signature") |
🤖 Prompt for AI Agents
In @backend/routes.py around lines 41 - 57, The helper function verify_signature
incorrectly uses Header(None) for x_hub_signature_256 (dependency injection only
works in route handlers); change verify_signature to accept x_hub_signature_256
as a normal parameter (e.g., x_hub_signature_256: str = None) and remove the
Header import/annotation, then update the github_webhook route to pass the
extracted header value into verify_signature when calling it so the header is
validated correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate required webhook headers explicitly.
The x_github_event header can be None, but there's no explicit validation. While the code handles None gracefully downstream (it won't match any event mapping), validating required headers upfront provides clearer error messages and better API hygiene.
🔎 Proposed fix
async def github_webhook(
request: Request,
- x_github_event: str = Header(None),
+ x_github_event: str = Header(...),
x_hub_signature_256: str = Header(None)
):Using Header(...) instead of Header(None) makes x_github_event required, and FastAPI will automatically return a clear 422 error if the header is missing.
🤖 Prompt for AI Agents
In @backend/routes.py at line 62, The x_github_event header parameter is
currently optional (Header(None)); make it required by replacing Header(None)
with Header(...) in the route signature where x_github_event: str is declared
(so FastAPI will validate and return a 422 when the header is missing), ensuring
the webhook handler parameter name x_github_event is updated in that function
signature and any callers/tests adjusted if needed.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add exception chaining for better error traceability.
When re-raising or raising a new exception within an except clause, use raise ... from err to preserve the exception chain, or raise ... from None to explicitly suppress it.
🔎 Proposed fix
try:
payload = json.loads(body.decode())
except json.JSONDecodeError:
- raise HTTPException(status_code=400, detail="Invalid JSON payload")
+ raise HTTPException(status_code=400, detail="Invalid JSON payload") from NoneBased on static analysis hints.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try: | |
| payload = json.loads(body.decode()) | |
| except json.JSONDecodeError: | |
| raise HTTPException(status_code=400, detail="Invalid JSON payload") | |
| try: | |
| payload = json.loads(body.decode()) | |
| except json.JSONDecodeError: | |
| raise HTTPException(status_code=400, detail="Invalid JSON payload") from None |
🧰 Tools
🪛 Ruff (0.14.10)
74-74: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
🤖 Prompt for AI Agents
In @backend/routes.py around lines 71 - 74, The JSON decode except block should
preserve the original exception chain: change the except to "except
json.JSONDecodeError as err:" and re-raise the HTTPException with "from err"
(e.g., raise HTTPException(status_code=400, detail="Invalid JSON payload") from
err) so the original json.loads/body.decode error is chained for traceability.
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,16 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Devr.AI</title> | ||
| </head> | ||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.tsx"></script> | ||
| </body> | ||
| </html> | ||
|
|
||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <link rel="icon" type="image/svg+xml" href="/DevrAI.svg" /> | ||
RishiGoswami-code marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Devr.AI</title> | ||
| </head> | ||
|
|
||
| <body> | ||
| <div id="root"></div> | ||
| <script type="module" src="/src/main.tsx"></script> | ||
| </body> | ||
|
|
||
| </html> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add warning log when signature verification is skipped.
Silently skipping signature verification creates a security risk. If accidentally deployed to production without a configured secret, the endpoint will accept unauthenticated webhook requests from any source.
🔎 Proposed fix
if not settings.github_webhook_secret: # If secret is not configured, we skip verification (not recommended for production) + logging.warning("GitHub webhook secret not configured - signature verification skipped. This is insecure for production!") return🤖 Prompt for AI Agents