Skip to content

Bug: Lightning address callback URL contains malformed webhook_data, breaking zaps #126

@santyr

Description

@santyr

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

  1. Create a pay link with a username (lightning address) and no webhook URL configured
  2. Query the lightning address endpoint:
    curl -s "https://example.com/.well-known/lnurlp/username" | jq .
  3. 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=None

The 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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions