Skip to content

Fix: personalize endpoint returns 500 on Apple Wallet retry#25

Open
Joseph-Cursio wants to merge 1 commit into
vapor-community:mainfrom
Joseph-Cursio:fix/personalize-retry-idempotency
Open

Fix: personalize endpoint returns 500 on Apple Wallet retry#25
Joseph-Cursio wants to merge 1 commit into
vapor-community:mainfrom
Joseph-Cursio:fix/personalize-retry-idempotency

Conversation

@Joseph-Cursio
Copy link
Copy Markdown

Summary

personalizedPass (PassesServiceCustom+RouteCollection.swift) calls personalization.create(on: req.db) without a read-guard or do/catch. PersonalizationInfo's migration declares .unique(on: passID), so a sequential retry of the same /personalize POST throws on the unique constraint and the response surfaces as 500 Internal Server Error.

Apple's Wallet Web Service Reference retries POST on non-2xx responses, so this turns a transient retry into a permanent failure loop. The same file already implements the symmetric pattern correctly for createRegistration (line 67-68: if r != nil { return .ok }).

Fix

Read PersonalizationInfoType for the given passID before constructing/inserting. If a row exists, skip the create and return the (deterministic) signed token directly. The token is the SHA-1 signature of userInfo.personalizationToken bytes, so an identical retry produces an identical response.

let existing = try await PersonalizationInfoType.query(on: req.db)
    .filter(\._$pass.\$id == id)
    .first()
if existing == nil {
    let personalization = PersonalizationInfoType()
    // ... existing field assignments ...
    try await personalization.create(on: req.db)
}

Test plan

  • Added a retry case to the existing Personalizable Pass Apple Wallet API test in Tests/VaporWalletPassesTests. After the first successful POST + assertions, issues a second POST with identical body and asserts:
    • res.status == .ok (not .internalServerError)
    • res.body.readableBytes > 0 (signed token still returned)
    • PersonalizationInfo.query(on: app.db).all().count == 1 (no duplicate row)
  • Inherits parameterization over useEncryptedKey: [true, false], so coverage applies to both code paths.
  • Verified the test fails without the fix with Expectation failed: (res.status → 500 Internal Server Error) == (.ok → 200 OK) on both parameterized cases. The Vapor warning log includes constraintUniqueFailed: UNIQUE constraint failed: personalization_info.pass_id, matching the diagnosis.
  • Verified the test passes with the fix, and the full `swift test` suite remains green (13 tests across 2 suites).

Notes

This was surfaced by a SwiftIdempotency road-test against this package — full findings under docs/wallet-package-trial/. The road-test was measurement-only (annotation surface, no logic edits); this PR is the separate upstream fix it identified.

Apple's Wallet Web Service Protocol retries POST on non-2xx responses.
The personalize endpoint calls personalization.create(on:) without a
read-guard or do/catch, and PersonalizationInfo's migration declares
.unique(on: passID), so a sequential retry hits the unique constraint
and surfaces as 500 — looping Apple's retry indefinitely rather than
satisfying it.

Fix: read first, mirroring the dedup-guard pattern already used by
createRegistration (line 67-68: 'if r != nil { return .ok }').

The added regression case in 'Personalizable Pass Apple Wallet API'
issues a second POST with identical body and asserts the response is
200 OK with a single PersonalizationInfo row, not 500. Without the
fix, the test fails with 'res.status → 500 Internal Server Error'
and Vapor logs 'constraintUniqueFailed: UNIQUE constraint failed:
personalization_info.pass_id'.
@Joseph-Cursio Joseph-Cursio requested a review from fpseverino as a code owner May 5, 2026 20:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant