-
Notifications
You must be signed in to change notification settings - Fork 16
Description
Description
When accessing a lightning address via /.well-known/lnurlp/<username>, the returned callback URL contains a malformed webhook_data parameter even when no webhook is configured. This breaks LNURL-pay flows (especially Nostr zaps) because clients append ?amount=... to a URL that already has query parameters, resulting in double question marks.
Steps to Reproduce
- Create a pay link with a username (lightning address) and no webhook URL configured
- Query the lightning address endpoint:
curl -s "https://example.com/.well-known/lnurlp/username" | jq .
- Observe the callback URL contains unexpected query params
Expected Behavior
{
"callback": "https://example.com/lnurlp/api/v1/lnurl/cb/xxxxxx",
...
}Actual Behavior
{
"callback": "https://example.com/lnurlp/api/v1/lnurl/cb/xxxxxx?webhook_data=alias%3D%27webhook_data%27+extra%3D%7B%7D",
...
}When a client (e.g., Primal, Damus) tries to pay, it appends ?amount=10000&nostr=... to this URL, resulting in:
/lnurl/cb/xxxxxx?webhook_data=alias%3D%27webhook_data%27+extra%3D%7B%7D?amount=10000&nostr=...
The double ? causes FastAPI to parse everything as one malformed parameter, returning:
RequestValidationError: [{'loc': ('query', 'amount'), 'msg': 'field required', 'type': 'value_error.missing'}]
Root Cause
In views_lnurl.py, the lnaddress() function calls api_lnurl_response() directly as a Python function without passing the webhook_data argument:
# Line 201-208
@lnurlp_lnurl_router.get("/api/v1/well-known/{username}")
async def lnaddress(
username: str, request: Request
) -> LnurlPayResponse | LnurlErrorResponse:
address_data = await get_address_data(username)
if not address_data:
return LnurlErrorResponse(reason="Lightning address not found.")
return await api_lnurl_response(request, address_data.id) # <-- missing webhook_data=NoneThe api_lnurl_response() function signature uses Query(None) as a default:
# Line 152-153
async def api_lnurl_response(
request: Request, link_id: str, webhook_data: str | None = Query(None)
) -> LnurlPayResponse:When called as a regular Python function (not via HTTP request), Query(None) doesn't resolve to None—it remains a FastAPI FieldInfo object. This object is truthy, so the check on line 170 passes:
# Line 169-171
url = request.url_for("lnurlp.api_lnurl_callback", link_id=link.id)
if webhook_data: # FieldInfo is truthy!
url = url.include_query_params(webhook_data=webhook_data) # str(FieldInfo) = "alias='webhook_data' extra={}"Proposed Fix
Option 1: Explicit None (minimal change)
@lnurlp_lnurl_router.get("/api/v1/well-known/{username}")
async def lnaddress(
username: str, request: Request
) -> LnurlPayResponse | LnurlErrorResponse:
address_data = await get_address_data(username)
if not address_data:
return LnurlErrorResponse(reason="Lightning address not found.")
return await api_lnurl_response(request, address_data.id, webhook_data=None)Option 2: Use Annotated syntax (recommended)
This separates the FastAPI metadata from the default value, making the function safe to call directly:
from typing import Annotated
@lnurlp_lnurl_router.get(
"/api/v1/lnurl/{link_id}",
name="lnurlp.api_lnurl_response.deprecated",
deprecated=True,
)
@lnurlp_lnurl_router.get(
"/{link_id}",
name="lnurlp.api_lnurl_response",
)
async def api_lnurl_response(
request: Request,
link_id: str,
webhook_data: Annotated[str | None, Query()] = None,
) -> LnurlPayResponse:Impact
- All lightning address zaps fail with 400 errors
- Regular LNURL-pay (non-lightning-address) works fine because it hits the endpoint via HTTP where FastAPI resolves
Query(None)correctly - Affects any client that uses the standard LNURL-pay flow (Primal, Damus, Zeus, etc.)
Environment
- LNbits with lnurlp extension
- FastAPI/Pydantic backend