DEV Community

grahamprice3746
grahamprice3746

Posted on

Password Reset Email API Requests: Tracing Timeout Boundaries Before Retry Decisions

An HTTP timeout does not tell you whether a password reset message was accepted, and treating it as permission to send another can produce two valid links. Short answer: put a deadline on the outbound request, record the reset attempt before dispatch, and reconcile an uncertain outcome against the mail system's acceptance record before issuing another message. This matters especially when the same notification pipeline also tells a marketplace seller about a new order: an order event and a reset attempt need distinct audit records, even if both use the same transport.

Why does a password reset email API request time out after dispatch?

Start with four timestamps: reset attempt created, outbound connection started, HTTP response received, and provider acceptance recorded. A missing response only narrows the diagnosis to the interval between the second and third timestamps; it does not establish that no message left the provider. An HTTP 202 response itself indicates acceptance for processing, not completion of processing, as RFC 9110 specifies. Conversely, a transport timeout says nothing definitive about what a remote service did after receiving the request. Keep those states separate in the incident record.

The word "hanging" conceals several boundaries. DNS resolution or a connection attempt may never finish; the server may read the request and fail to answer promptly; or the application may receive an acknowledgment but hold the user-facing request open while awaiting downstream work. Record elapsed time around each boundary, plus a locally generated attempt identifier and any returned remote message identifier. Do not log the reset token, the reset URL, or the full response body: those can contain credentials. A reset endpoint also needs a consistent user-facing response for existing and nonexistent accounts, so diagnostics belong in protected telemetry rather than in different client-visible errors; the OWASP Forgot Password Cheat Sheet explicitly calls for consistent messages and response times.

Two minutes of silence is not evidence of two minutes of delivery latency. It may be two minutes spent waiting for an HTTP response. Measure those separately.

No response is not a rejection.

Make the timeout an explicit state transition

In Node.js, fetch accepts an abort signal; a timer-backed signal gives the request an upper bound. Axios documents a timeout option for its requests and recommends combining it with cancellation for connection-related hangs. Pick one client in the running service, set its deadline in one place, and make the retry policy explicit rather than relying on whatever an SDK happens to do by default. The following Go sketch expresses the transport-independent part of the design; its store and sender are interfaces to be implemented with the application's existing persistence and HTTP stack.

package reset

import (
    "context"
    "time"
)

type Store interface {
    RecordAttempt(ctx context.Context, attemptID, accountID string) error
    MarkUnknown(ctx context.Context, attemptID string) error
    MarkAccepted(ctx context.Context, attemptID, remoteID string) error
}

type Sender interface {
    Send(ctx context.Context, attemptID string) (remoteID string, err error)
}

func Dispatch(ctx context.Context, store Store, sender Sender, attemptID, accountID string) error {
    if err := store.RecordAttempt(ctx, attemptID, accountID); err != nil {
        return err
    }
    requestCtx, cancel := context.WithTimeout(ctx, 8*time.Second)
    defer cancel()
    remoteID, err := sender.Send(requestCtx, attemptID)
    if err != nil {
        if recordErr := store.MarkUnknown(ctx, attemptID); recordErr != nil {
            return recordErr
        }
        return err
    }
    return store.MarkAccepted(ctx, attemptID, remoteID)
}
Enter fullscreen mode Exit fullscreen mode

Eight seconds here is an illustrative application deadline, not a service-level fact or universal recommendation. The sketch also exposes an important implementation constraint: when ctx is canceled by an upstream client, writing the outcome with that same context may fail. A production worker needs its own bounded persistence context, and state writes need transactional rules so a crash between remote acceptance and the local update remains reconcilable. Do not represent unknown as failed; it is the very state the audit trail exists to preserve.

How should an uncertain send be reconciled?

First search the provider's event or message records using the attempt identifier, if its API supports a caller-supplied correlation field. If no such field exists, map the returned message identifier when available and retain the time window, destination fingerprint, and template type in local records. Whether a provider offers idempotency keys, searchable metadata, or delivery events is an integration capability to verify before relying on it. No local fetch or Axios retry option can manufacture remote exactly-once semantics.

Acceptance, delivery, and use are three different observations. A provider acknowledgment establishes only that the provider accepted work. A delivery event has the semantics documented by that provider, while a successful reset is established by the account system's token-consumption record. For security, issue a single-use, expiring token and rate-limit reset requests, consistent with OWASP's reset guidance. If an uncertain attempt cannot be correlated to a remote record, decide whether to issue a new attempt under a documented resend policy; do not silently replay the same outbound operation in a tight loop. Preserve an append-only sequence of decisions: initial request, timeout, reconciliation lookup, operator or automated resend decision, and token consumption. This is the same accounting discipline that keeps a seller's new-order notification from being mistaken for a second order.

Consider the narrow case in which the remote endpoint accepts the reset email request, then its acknowledgment is lost on the return path: the local timeout fires, the local record moves to unknown, and a delivery event may arrive later. An immediate second request might then create another message while the first link remains usable. A reconciliation worker should match the event to the original attempt, record the remote identifier and observation time, and close the uncertain state without inventing a second dispatch. If no event arrives within the documented observation window, the worker still cannot prove non-delivery solely from silence; it must apply the application's explicit resend policy and record that decision. The corresponding seller-order case should use its own event identifier, so replaying an order event never creates an unrelated password reset attempt.

The distinction survives a restart.

There is a genuine trade-off here. Deferring a resend while reconciling can slow a legitimate recovery; retrying immediately can deliver duplicate messages, and a newly generated token may invalidate a link still in flight. Set an explicit reconciliation window and a user-requested resend path, then test both against your token invalidation rules. Compliance obligations depend on jurisdiction and on the message's purpose; do not infer that a password reset or order notice is automatically exempt from every messaging or retention requirement. RFC 8058, for instance, describes a one-click unsubscribe mechanism for applicable mailing-list messages, not a universal rule for transactional resets.

Compare the contracts, then roll out

Compare integrations by what can be observed after uncertainty: can an attempt be correlated across local logs and remote records, are acceptance and delivery distinct, how long are events retained, and can retries be controlled without duplicating a message? Also check connection and response deadlines, webhook verification, access controls for event history, operational cost of retaining audit data, and the engineering effort required to exercise these paths in a staging environment. A fast happy-path response is a poor substitute for evidence at the ambiguous boundary.

Roll out with a small set of failure-injection cases: abort before connection, abort after the request is transmitted, lose the acknowledgment, and delay the delivery event. Assert the resulting state transitions and verify that an unknown attempt never becomes a confirmed failure merely because the caller timed out. Then enable bounded dispatch for a limited cohort, monitor unknown attempts and reconciliation lag separately from accepted and delivered counts, and expand only when operators can trace a particular reset attempt without exposing its token. Keep the user-facing reset response stable throughout.

References

The HTTP acceptance semantics, Node.js cancellation behavior, Axios request deadline options, OWASP reset guidance, and RFC 8058 scope underpin the distinctions above. Provider-specific event retention and idempotency behavior must be checked against the chosen integration's contract.

Sources

Top comments (0)