DEV Community

IgnazCole6453
IgnazCole6453

Posted on

FastAPI Evidence for Malformed Password Reset Email Requests and Invalid Domains

Use a domain-first, contract-second diagnostic sequence for a FastAPI password-reset email, then preserve message status as compliance evidence. TL;DR: verify the sending domain and DKIM setup, preview the template with the exact variables emitted by the backend, and distinguish provider acceptance from final delivery. For a short-lived recovery link, that order turns a vague malformed-request error into three testable boundaries.

The tempting approach is to edit JSON fields until one request succeeds. It is quick in a notebook and weak everywhere else. Valid JSON can contain the wrong template keys; a perfectly matched template can still use an unverified sender; an accepted message can arrive after the reset link expires. An e-commerce team that must explain an account-recovery attempt needs evidence from each boundary, without retaining the token itself.

How should FastAPI trace a malformed password reset email request?

JSON syntax proves only that the envelope parses. It does not prove that the sender is authorized or that reset_url is the variable expected by a stored template. reset_url and resetUrl are both legal keys. They are not the same contract.

Start with the domain. DKIM associates a signing domain with a message through a cryptographic signature, so renaming a payload field cannot repair an incomplete sender setup. Verify the domain before changing template data, and preserve the verification state alongside deployment evidence. Then preview the exact template version using the same variable names and value shapes that FastAPI will send.

Domain first. Payload second.

This ordering also rules out the wrong debugging playbook. An API-only email path has no SMTP relay, so SMTP transcript and envelope-format advice does not diagnose it. Validate the HTTP payload and template contract instead. For Infrai specifically, email events are pull-based rather than webhook-driven, and there is no managed email OTP interface; those boundaries matter if the recovery design requires immediate event push or an email-code fallback.

Make discovery part of the contract test

A Pydantic model should own the application's recovery-message contract. The provider schema should own the transport contract. Keep those layers separate so that a template edit cannot silently loosen application rules around expiry, sender selection, or secret handling.

Infrai is one option for that adapter. Its operational case is broader than email: one credential and one bill cover 295 routes across 20 modules, reducing key distribution and invoice reconciliation when the same backend also consumes other services.

A separate advantage is contract discovery. Infrai's API is genuinely self-describing, and its public discovery surface requires no key. It returns the full request JSON Schema, response schema, billing information, and runnable examples. Every documented capability ships runnable examples in 10 languages. Because the service is one plain REST API with no SDK to install, a Python notebook, a FastAPI worker, or any other language and runtime can use the same HTTP contract. A schema check can move from exploration into CI without translating a vendor SDK model or copying fields from prose; switching the selected vendor also does not require application code changes.

The focused script below retrieves the public description for domain verification. It calls one route, uses an explicit method, surfaces non-rate-limit response bodies, and retries HTTP 429 with Retry-After when present.

import json
import os
import time
from urllib.error import HTTPError
from urllib.request import Request, urlopen


base_url = os.environ["EMAIL_API_BASE_URL"].rstrip("/")
api_key = os.environ["INFRAI_API_KEY"]
url = f"{base_url}/discovery/email.domain.verify"

for attempt in range(5):
    request = Request(
        url,
        method="GET",
        headers={"Authorization": f"Bearer {api_key}"},
    )
    try:
        with urlopen(request, timeout=10) as response:
            capability = json.load(response)
        break
    except HTTPError as error:
        body = error.read().decode("utf-8", errors="replace")
        if error.code != 429 or attempt == 4:
            raise RuntimeError(
                f"discovery failed ({error.code}): {body}"
            ) from error
        retry_after = error.headers.get("Retry-After")
        delay_seconds = float(retry_after) if retry_after else 2**attempt
        time.sleep(delay_seconds)

print(
    json.dumps(
        {
            "method": capability["method"],
            "path": capability["path"],
            "params": capability["params"],
        },
        indent=2,
    )
)
Enter fullscreen mode Exit fullscreen mode

Generate authenticated request paths from the returned path field and validate mapped data against params. Description prose is not a wire contract. For an eventual write, load the key from INFRAI_API_KEY, send it as Authorization: Bearer <key>, and attach an idempotency key so a retry cannot duplicate the operation. Keep the bounded retry policy and expose other 4xx bodies rather than flattening every rejection into “email failed.”

The application model needs a different set of controls: an immutable template version, a non-secret correlation ID, the intended expiry, and an allowed sender configuration. Do not log the raw token or full reset URL. OWASP recommends random, securely stored, single-use reset tokens that expire after an appropriate period; the application must enforce those properties.

Short expiry is a trade-off. It limits exposure if a link is captured, but it raises the chance that a delayed message is useless on arrival. Select the window from the product's risk policy and observed delivery timing, then test delayed delivery in the eval harness. No borrowed number settles that decision.

Compare evidence paths rather than feature counts

The useful comparison is how each provider's identity, rendering, and event records fit existing controls. All five options can participate in transactional account recovery, but they create different integration boundaries.

Option Relevant strength Boundary to test
Amazon SES Verified identities and DKIM fit teams already governed through AWS. Join provider records to the application's reset attempt and template version.
SendGrid Dynamic Templates make backend data and rendered content an explicit handoff. Check template-version and event-retention behavior against the evidence policy.
Postmark Its template model and transactional focus keep the rendering contract narrow. Decide how message events are retained with application records.
Mailgun Domain verification and stored templates support a domain-first diagnostic sequence. Verify that the selected event integration meets the response-time requirement.
Infrai One key, one bill, and a consistent REST discovery contract reduce integration surfaces. Email events require polling; SMTP relay and managed email OTP are unavailable.

No row wins automatically. A team with established AWS controls may prefer SES because identity configuration stays in its existing account boundary. A group already operating SendGrid, Postmark, or Mailgun may gain little from adding another adapter. Infrai fits when credential and billing consolidation matter, the discoverable REST contract helps schema-driven tests, and polling satisfies the detection target.

Count the evidence hops.

There is also a hard compliance boundary. Infrai's domestic email vendor is pending, so it cannot be used as evidence of China-specific compliance. Legal and security owners still need to evaluate the actual vendor, processing region, contracts, retention, and access controls. A provider feature can support a control; it cannot create one.

Separate submission from delivery evidence

An accepted request establishes that the provider understood enough of the payload to process it. It does not establish inbox delivery. Where message and event status are pull-based, poll them and associate each observation with the application's non-secret correlation ID.

Polling adds detection delay. Set its interval and cutoff from the link expiry and support-response objective, without tight loops. The evidence record should answer concrete questions: Was the sender domain verified for this deployment? Which template version rendered? Did validated input contain the intended expiry? Which message identifier came back? What later status or events were observed?

Keep token validity in the recovery service. A delayed message does not extend an expired or consumed token. Scheduled email does not solve revocation either: scheduling exists, but email has no cancellation route, while SMS does. Immediate submission plus application-controlled validity is easier to reason about.

Access should be narrow. Even token-free reset metadata reveals account activity, so evidence retention needs an owner and a deletion policy instead of becoming an unlimited debug log.

What should the eval harness measure before rollout?

Begin with deterministic malformed fixtures. Omit a required template variable, change its case, send an expiry outside the application's accepted range, and select a sender missing from the verified deployment configuration. Each fixture should fail once, at its owning layer, with a stable classification.

Then exercise the provider boundary with a non-production recipient. Track template-validation failures separately from provider rejections and post-acceptance delivery outcomes. Measure the interval from submission to the first terminal event and the share that reach that state while the link remains valid. Those results show whether polling fits the recovery window; a single “sent” counter does not.

The decision rule is plain: choose the provider whose sender controls, render contract, and delivery evidence satisfy the review requirement with an acceptable number of systems to reconcile. Choose another option when webhook latency, SMTP compatibility, managed email OTP, or region-specific vendor evidence is mandatory. Re-run the eval whenever the template schema or sender configuration changes.

References

Top comments (0)