{"record":{"id":"b9879789a56b904f","repo":"signalapp/Signal-Server","slug":"invalid-receipt-credential-request-b98797","errorCode":null,"errorMessage":"invalid receipt credential request","messagePattern":"invalid receipt credential request","errorType":"exception","errorClass":"SubscriptionInvalidArgumentsException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriptionManager.java","lineNumber":207,"sourceCode":"   * @throws SubscriptionReceiptAlreadyRedeemedException         if the receipt was already redeemed by a different\n   *                                                             request\n   * @throws RateLimitExceededException                          if rate-limited\n   */\n  public ReceiptResult createReceiptCredentials(\n      final SubscriberCredentials subscriberCredentials,\n      final byte[] receiptCredentialRequestBytes,\n      final Function<CustomerAwareSubscriptionPaymentProcessor.ReceiptItem, Instant> expiration)\n      throws SubscriptionForbiddenException, SubscriptionNotFoundException, SubscriptionInvalidArgumentsException, SubscriptionPaymentRequiredException, RateLimitExceededException, SubscriptionReceiptRequestedForOpenPaymentException, SubscriptionReceiptAlreadyRedeemedException {\n    final Subscriptions.Record record = getSubscriber(subscriberCredentials);\n    if (record.subscriptionId == null) {\n      throw new SubscriptionNotFoundException();\n    }\n\n    final ReceiptCredentialRequest receiptCredentialRequest;\n    try {\n      receiptCredentialRequest = new ReceiptCredentialRequest(receiptCredentialRequestBytes);\n    } catch (final InvalidInputException e) {\n      throw new SubscriptionInvalidArgumentsException(\"invalid receipt credential request\", e);\n    }\n\n    final PaymentProvider processor = record.getProcessorCustomer().orElseThrow().processor();\n    final SubscriptionPaymentProcessor manager = getProcessor(processor);\n    final SubscriptionPaymentProcessor.ReceiptItem receipt = manager.getReceiptItem(record.subscriptionId);\n    final Instant expirationInstant = expiration.apply(receipt);\n    final ReceiptCredentialResponse receiptCredentialResponse;\n    try {\n      issuedReceiptsManager\n          .recordIssuance(receipt.itemId(), manager.getProvider(), receiptCredentialRequest, expirationInstant);\n      receiptCredentialResponse = zkReceiptOperations.issueReceiptCredential(\n          receiptCredentialRequest,\n          expirationInstant.getEpochSecond(),\n          receipt.level());\n    } catch (final VerificationFailedException e) {\n      throw new SubscriptionInvalidArgumentsException(\"receipt credential request failed verification\", e);\n    } catch (final WriteConflictException _) {\n      throw new SubscriptionReceiptAlreadyRedeemedException();","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriptionManager.java#L189-L225","documentation":"createReceiptCredentials throws SubscriptionInvalidArgumentsException(\"invalid receipt credential request\") when the client-supplied receiptCredentialRequest bytes cannot be parsed into a ReceiptCredentialRequest (InvalidInputException from the zk constructor). The payload is malformed, not merely wrong — it fails structural deserialization before any ZK verification.","triggerScenarios":"Posting a base64 receiptCredentialRequest body whose decoded bytes do not form a valid ReceiptCredentialRequest: wrong length, wrong serialization, garbage data, or bytes from a different protocol version.","commonSituations":"Client built the request with a mismatched libsignal/zk library version; the base64 payload got truncated or re-encoded by an intermediary; a test sent an arbitrary string instead of a real serialized request.","solutions":["Regenerate the ReceiptCredentialRequest client-side with a current, version-matched libsignal/zk receipt library and resend.","Ensure the client sends the raw serialized bytes (correct base64 with padding intact) — no truncation, whitespace, or URL-safe alphabet mismatches.","Log the byte length of the received payload and compare against the expected ReceiptCredentialRequest size to spot transport corruption."],"exampleFix":"// before\nString req = Base64.getEncoder().encodeToString(requestBytes).replace('+', '-'); // url-safe alphabet changed payload\n// after\nString req = Base64.getEncoder().encodeToString(new ReceiptCredentialRequest(serialized).serialize());","handlingStrategy":"validation","validationCode":"byte[] decoded = Base64.getDecoder().decode(requestBase64);\nif (decoded.length != new ReceiptCredentialRequest(validSample).serialize().length) {\n  throw new IllegalArgumentException(\"receiptCredentialRequest has unexpected byte length\");\n}","typeGuard":"boolean isValidReceiptCredentialRequest(byte[] bytes) {\n  try { new ReceiptCredentialRequest(bytes); return true; } catch (InvalidInputException e) { return false; }\n}","tryCatchPattern":"try {\n  manager.createReceiptCredentials(creds, requestBytes);\n} catch (SubscriptionInvalidArgumentsException e) {\n  log.warn(\"malformed receipt request — regenerate client-side\");\n}","preventionTips":["Keep client zk/libsignal library versions in sync with the server","Use standard base64 (not URL-safe) and preserve padding through transports","Sanity-check serialized payload length before sending"],"tags":["zk-receipts","deserialization","invalid-input"],"backgroundTag":"invalid-argument-format","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}