signalapp/Signal-Server · error · SubscriptionInvalidArgumentsException
purchase has no line items
Error message
purchase has no line items
What it means
getLineItem expects a ProductPurchaseV2 to contain exactly one product line item; a purchase with a null or empty productLineItem list cannot be mapped to a subscription/product state, so Signal throws SubscriptionInvalidArgumentsException. (If more than one exists it only logs a warning and uses the first.) An empty list means the Play API returned a purchase record without any active line item data.
Solutions
- Treat this as an expired/voided purchase: have the user re-subscribe and submit a fresh purchase token.
- Check the purchase's acknowledgement/voided state via the Play Developer API; voided purchases legitimately have no line items.
- Verify the client sends the purchaseToken for the right product/package (wrong token can yield empty records).
- If it is transient, re-query the Play API; occasionally line items appear only after purchase processing completes.
Example fix
// before: trusting a cached/stale token
client.validateToken(stalePurchaseToken);
// after: refresh purchase record first
PurchaseResult p = billingClient.queryPurchasesAsync(product);
if (!p.lineItems.isEmpty()) {
client.validateToken(p.purchaseToken);
} Defensive patterns
Strategy: validation
Validate before calling
ProductPurchaseV2 p = playApi.getPurchase(token);
if (p == null || p.getProductLineItem() == null || p.getProductLineItem().isEmpty()) {
treatAsExpiredOrVoided();
return;
} Type guard
function hasLineItems(purchase) { return Array.isArray(purchase?.productLineItem) && purchase.productLineItem.length > 0; } Try / catch
try {
api.validateToken(purchaseToken);
} catch (SubscriptionInvalidArgumentsException e) {
requireNewPurchase(); // line items are gone; purchase is expired/voided
} Prevention
- Query the purchase record and check for line items before submitting the token
- Treat voided/refunded purchases as ineligible and require repurchase
- Ensure the purchase token belongs to the correct package/product
When it happens
Trigger: Calling purchase/lineItem/expired paths with a purchase token whose ProductPurchaseV2 response from the Google Play Developer API contains no productLineItem entries — typically an expired, voided, or malformed purchase.
Common situations: Purchase was refunded/voided so Play drops the line item; token submitted long after expiry; incorrect package/product configuration making the API return an empty purchase record; Play API version mismatch returning v2 shape differences.
Understand the failure class
Background: EmptyResultError / "no results found": when an API or scraper succeeds but returns zero rows — this error's family across 9 libraries.
Related errors
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/571a2d0729f2ae87.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/GooglePlayBillingManager.java:369
.flatMap(ConsumptionState::fromString)
.orElseThrow(() -> new IllegalStateException("Purchase did not contain a consumption state: " + lineItem.getProductOfferDetails()));
if (consumptionState == ConsumptionState.YET_TO_BE_CONSUMED) {
// Mark this token as consumed
executeTokenOperation(publisher ->
publisher.purchases().products().consume(packageName, productId, purchaseToken));
}
}
return Optional.of(new PaymentDetails(purchaseToken, level, paymentStatus, purchaseTime, null));
} catch (SubscriptionNotFoundException e) {
return Optional.empty();
}
}
private ProductLineItem getLineItem(final ProductPurchaseV2 purchase) throws SubscriptionInvalidArgumentsException {
final List<ProductLineItem> lineItems = purchase.getProductLineItem();
if (lineItems == null || lineItems.isEmpty()) {
throw new SubscriptionInvalidArgumentsException("purchase has no line items");
}
if (lineItems.size() > 1) {
logger.warn("{} line items found for purchase {}, expected 1", lineItems.size(), purchase.getOrderId());
}
return lineItems.getFirst();
}
interface ApiCall<T> {
AndroidPublisherRequest<T> req(AndroidPublisher publisher) throws IOException;
}
/**
* Asynchronously execute a synchronous API call on a purchaseToken, mapping expected errors to the appropriate
* {@link SubscriptionException}
*
* @param apiCall An API call that operates on a purchaseToken
* @param <R> The result of the API callView on GitHub (pinned to 100ab61c82)