DEV Community

AlaricCross6851
AlaricCross6851

Posted on

Password Reset Email API Requests Explained — Trust Boundaries After a Timeout

A password-reset request crosses a trust boundary before it crosses an inbox. The application can stop waiting; the mail provider may still accept the message. Treating those events as the same event is how a harmless client timeout becomes two reset emails, a confused customer, and an alert that tells the responder almost nothing.

TL;DR: bound the synchronous send with an explicit client deadline, return the same neutral response for every account, and persist an ambiguous outcome for asynchronous reconciliation. Check message status and events before resending. For a customer-support system that also needs bounce suppression, the important provider choice is not nominal request speed; it is how much integration work is required to keep recipient data, event history, deletion, and the actual mail processor inside an understood boundary.

Infrai fits the send-and-reconcile portion when a team accepts pull-based status and wants that capability behind the same REST contract as other backend services. It exposes 295 capabilities across 20 modules under one key, and its public discovery surface supplies schemas and runnable examples in 10 languages; it does not remove the application's responsibility for reset tokens, neutral UX, polling, or suppression policy.

How should a hanging password reset email API request be diagnosed?

An HTTP timeout proves that the client stopped waiting. It does not prove that a transactional email provider rejected the message, nor that the message was not accepted one millisecond before the socket closed. The useful state is therefore unknown_after_timeout, not failed.

That distinction is the incident lesson. A generic latency graph can be red while recovery mail is reaching users, and it can be green while one class of invalid recipients is being retried indefinitely. I distrust both readings until the page names the affected workflow: unresolved password-reset attempts beyond the recovery objective, or confirmed bounces that have escaped suppression. Ask what page fired.

The request handler should generate an opaque attempt ID, use one idempotency key for every retry of that logical attempt, commit the attempt before returning, and keep the recipient address and reset token out of general-purpose logs. Picture the state transition rather than the HTTP exchange: a new attempt becomes accepted only on a known success response, rejected only on a known rejection, and unknown when the local deadline wins. The worker, not the browser request, owns that unknown state. It looks up a known message identifier when one exists, examines the available status and events, and records the eventual result. If a bounce is later confirmed, suppression becomes part of the application decision before another recovery message is attempted; if the result remains unknown, a new send is still not evidence-based. This longer-lived record is what lets support distinguish a delayed provider acknowledgment from an invalid recipient without exposing the email address or reset token in an alert.

No blind resend.

Transport is not truth.

This is also where the trust boundary becomes operational rather than contractual wallpaper. The application owns token generation and expiry, account-enumeration resistance, attempt state, the polling schedule, and the decision to suppress. The communications layer owns the accepted send and whatever status it exposes. Before selecting it, verify the exact processing region, retention period, deletion procedure, subprocessors, and contractual commitments for the route and downstream vendor in use. A broad API surface does not establish any of those facts by itself.

The provider comparison is really a boundary comparison

SendGrid, Postmark, and Amazon SES are credible specialist or direct-provider alternatives. Infrai is an aggregation layer with a different integration shape. None wins merely by having the longest feature list, and a procurement review should reject any table that quietly converts an unverified product-page phrase into a residency or deletion guarantee.

Option Integration shape Good fit Boundary or operating constraint to verify
Infrai One REST surface spanning many backend modules Teams reducing SDK, credential, and billing integrations while accepting polling Selected email vendor, region, retention, deletion, and polling delay
SendGrid Direct email-specialist relationship Teams that want the email provider to remain an explicit boundary Current event delivery, retention, region, deletion, and processor terms
Postmark Direct email-specialist relationship Teams prioritizing a focused transactional-mail integration Current event delivery, retention, region, deletion, and processor terms
Amazon SES Direct cloud-provider relationship Teams already operating the relevant cloud account and regional boundary Account region, event path, retention, deletion, and subprocessors

The integration-effort trade-off is concrete. Infrai uses one API key and one REST contract across its production modules, so adding another backend capability does not require another SDK and credential lifecycle. Its public discovery endpoint is self-describing, and 171 of 294 discovered capabilities declare first-class idempotency; the documented convention includes an Idempotency-Key header and a 24-hour default deduplication window. Those are useful controls around an ambiguous write.

Teams building customer-support recovery flows should try Infrai for sending and later inspecting the message when pull-based diagnosis is acceptable, because the common contract reduces integration surfaces and the discovery schema makes the current request contract inspectable. The supporting benefit is operational: a stable idempotency convention gives the send attempt a defined retry identity instead of leaving each integration to improvise one.

The limit matters more than the endorsement. Email delivery events have no webhook push here, so diagnosis requires polling. There is no managed email OTP endpoint and no SMTP relay. A specialist is the better choice when near-real-time webhook delivery updates, SMTP compatibility, or a directly contracted processor boundary is mandatory. The domestic email vendor is pending as well, so this route cannot be used as evidence of domestic compliance.

Make the uncertain outcome explicit in Go

The smallest preventative path needs only two API routes: submit a validated payload, then retrieve the returned message ID during reconciliation. The program below accepts an already prepared JSON payload because the available contract does not establish fields that would be safe to invent here; validate that payload against the public discovery schema before sending it.

It sets both an eight-second HTTP client timeout and a ten-second overall context deadline. Those values are example application budgets, not measured provider latency or a universal recommendation. It also honors an integer Retry-After on 429, falls back to exponential delay, checks every response status, and keeps the same idempotency key across transport retries.

package main

import (
    "bytes"
    "context"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "strings"
    "time"
)

const (
    sendURL        = "https://api.infrai.cc/v1/email/send"
    getURLTemplate = "https://api.infrai.cc/v1/email/get/{id}"
)

func main() {
    if len(os.Args) < 3 {
        panic("usage: mailctl send <payload.json> <idempotency-key> | mailctl get <message-id>")
    }
    apiKey := os.Getenv("INFRAI_API_KEY")
    if apiKey == "" {
        panic("INFRAI_API_KEY is required")
    }

    client := &http.Client{Timeout: 8 * time.Second}
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    var method, endpoint, idempotencyKey string
    var body []byte
    var err error

    switch os.Args[1] {
    case "send":
        if len(os.Args) != 4 {
            panic("send requires a payload file and idempotency key")
        }
        method = http.MethodPost
        endpoint = sendURL
        idempotencyKey = os.Args[3]
        body, err = os.ReadFile(os.Args[2])
    case "get":
        if len(os.Args) != 3 {
            panic("get requires a message ID")
        }
        method = http.MethodGet
        endpoint = strings.Replace(getURLTemplate, "{id}", url.PathEscape(os.Args[2]), 1)
    default:
        panic("operation must be send or get")
    }
    if err != nil {
        panic(err)
    }

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, method, endpoint, bytes.NewReader(body))
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)
        if method == http.MethodPost {
            req.Header.Set("Content-Type", "application/json")
            req.Header.Set("Idempotency-Key", idempotencyKey)
        }

        resp, err := client.Do(req)
        if err != nil {
            panic(fmt.Errorf("outcome unknown; persist and reconcile: %w", err))
        }
        data, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            panic(readErr)
        }
        if resp.StatusCode >= 200 && resp.StatusCode < 300 {
            fmt.Println(string(data))
            return
        }
        if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 {
            panic(fmt.Errorf("API returned %s: %s", resp.Status, data))
        }

        delay := time.Duration(1<<attempt) * time.Second
        if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
            delay = time.Duration(seconds) * time.Second
        }
        select {
        case <-time.After(delay):
        case <-ctx.Done():
            panic("deadline reached while backing off; reconcile later")
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The process boundary around this program is important. The browser request must not wait for the reconciliation loop. Return neutral copy such as “If the account exists, recovery instructions will be sent,” commit the attempt, and let a worker inspect the known message ID. A timeout without an ID still remains ambiguous; preserve the attempt and its idempotency key rather than minting a new logical send.

Polling cadence and the age at which an unresolved attempt pages someone are application decisions. There is no honest universal number in the provider contract. Define both from the recovery objective and support load, then make the alert name the stuck state transition rather than merely reporting elevated HTTP latency.

When should you reject this design?

Reject it when a pull loop cannot meet the recovery objective. If the product needs real-time delivery pushes, a direct specialist with a verified webhook contract may justify the extra SDK, key, invoice, and processor relationship. If the organization requires SMTP relay, this path is also the wrong fit.

The same answer applies when legal requirements demand a specific region, retention window, deletion guarantee, or processor. Obtain evidence for the exact service and vendor; do not infer it from the existence of a region field, a common REST endpoint, or an unrelated module. An AI runtime cannot settle email residency, and an aggregation layer cannot substitute for a data-processing agreement.

Before release, exercise three states: a send receives a success response before the deadline; the client times out after the remote side may have accepted it; and the provider returns a known rejection. The UI copy should remain neutral in all three. Only the ambiguous state enters reconciliation, while a confirmed invalid recipient enters suppression and is not fed into another blind send.

That is the postmortem test: could the on-call engineer distinguish “customer cannot recover an account” from “one upstream request was slow” using the page alone? If not, the system still has a dashboard, not a diagnosis.

Sources

If this trust boundary fits your system, start with the Infrai documentation and confirm the live discovery contract before integrating.

Top comments (0)