Skip to content

Commit 704be5d

Browse files
committed
Fall back to deserializeUnsafe when webhook api_version doesn't match
the Stripe SDK
1 parent c4a99f1 commit 704be5d

3 files changed

Lines changed: 93 additions & 34 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*-
2+
* ========================LICENSE_START=================================
3+
* restheart-stripe
4+
* %%
5+
* Copyright (C) 2019 - 2026 SoftInstigate
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* =========================LICENSE_END==================================
19+
*/
20+
package org.restheart.stripe.webhook;
21+
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
25+
import com.stripe.Stripe;
26+
import com.stripe.exception.EventDataObjectDeserializationException;
27+
import com.stripe.model.Event;
28+
import com.stripe.model.StripeObject;
29+
30+
/**
31+
* Deserialization of the {@code data.object} payload of a Stripe event.
32+
*/
33+
final class EventPayloads {
34+
35+
private static final Logger LOGGER = LoggerFactory.getLogger(EventPayloads.class);
36+
37+
private EventPayloads() {
38+
}
39+
40+
/**
41+
* Deserializes the event payload as {@code type}.
42+
*
43+
* <p>{@code EventDataObjectDeserializer.getObject()} returns an empty {@code Optional}
44+
* without raising anything when the event's {@code api_version} differs from the SDK's
45+
* {@link Stripe#API_VERSION}. That mismatch is the normal state of affairs, not an edge
46+
* case: the API version is pinned on the Stripe account and drifts from the bundled SDK
47+
* whenever either side is upgraded. This falls back to {@code deserializeUnsafe()}, which
48+
* skips the version check, as recommended by Stripe.
49+
*
50+
* @param event the verified Stripe event
51+
* @param type the expected payload type
52+
* @return the deserialized payload, or {@code null} if it could not be deserialized
53+
* or is not of the expected type
54+
*/
55+
@SuppressWarnings("unchecked")
56+
static <T extends StripeObject> T deserialize(Event event, Class<T> type) {
57+
var deserializer = event.getDataObjectDeserializer();
58+
59+
var payload = deserializer.getObject().orElse(null);
60+
61+
if (payload == null) {
62+
LOGGER.debug("[stripe] event {} has api_version {} but the SDK is pinned to {}; retrying with deserializeUnsafe()",
63+
event.getType(), event.getApiVersion(), Stripe.API_VERSION);
64+
try {
65+
payload = deserializer.deserializeUnsafe();
66+
} catch (EventDataObjectDeserializationException e) {
67+
LOGGER.error("[stripe] could not deserialize {} payload as {} (event api_version {}, SDK api_version {})",
68+
event.getType(), type.getSimpleName(), event.getApiVersion(), Stripe.API_VERSION, e);
69+
return null;
70+
}
71+
}
72+
73+
if (!type.isInstance(payload)) {
74+
LOGGER.error("[stripe] {} payload is a {}, expected a {}",
75+
event.getType(), payload.getClass().getSimpleName(), type.getSimpleName());
76+
return null;
77+
}
78+
79+
return (T) payload;
80+
}
81+
}

stripe/src/main/java/org/restheart/stripe/webhook/OrderEventHandler.java

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
import com.mongodb.client.model.Filters;
4242
import com.mongodb.client.model.Updates;
4343
import com.stripe.model.Event;
44-
import com.stripe.model.StripeObject;
4544
import com.stripe.model.Charge;
4645
import com.stripe.model.Dispute;
4746
import com.stripe.model.checkout.Session;
@@ -108,7 +107,7 @@ public void handle(Event event, StripeEventContext ctx) throws Exception {
108107
// ── Handlers ─────────────────────────────────────────────────────────────
109108

110109
private void handleSessionCompleted(Event event, StripeEventContext ctx, ProductsConfig products) {
111-
var session = deserialize(event, Session.class);
110+
var session = EventPayloads.deserialize(event, Session.class);
112111
if (session != null) {
113112
if (!"paid".equals(session.getPaymentStatus())) {
114113
LOGGER.info("[stripe] checkout.session.completed with status '{}' — leaving as pending_payment",
@@ -138,7 +137,7 @@ private void handleSessionCompleted(Event event, StripeEventContext ctx, Product
138137

139138
private void handleAsyncPaymentSucceeded(Event event, StripeEventContext ctx, ProductsConfig products) {
140139
LOGGER.info("[stripe] handleAsyncPaymentSucceeded: starting");
141-
var session = deserialize(event, Session.class);
140+
var session = EventPayloads.deserialize(event, Session.class);
142141
if (session == null) {
143142
LOGGER.info("[stripe] handleAsyncPaymentSucceeded: session is null, trying fallback extraction");
144143
// Fallback: extract directly from JSON when SDK deserialization fails (API version mismatch)
@@ -158,7 +157,7 @@ private void handleAsyncPaymentSucceeded(Event event, StripeEventContext ctx, Pr
158157
}
159158

160159
private void handleAsyncPaymentFailed(Event event, StripeEventContext ctx, ProductsConfig products) {
161-
var session = deserialize(event, Session.class);
160+
var session = EventPayloads.deserialize(event, Session.class);
162161
String sessionId;
163162
if (session != null) {
164163
sessionId = session.getId();
@@ -186,7 +185,7 @@ private void handleAsyncPaymentFailed(Event event, StripeEventContext ctx, Produ
186185
}
187186

188187
private void handleSessionExpired(Event event, StripeEventContext ctx, ProductsConfig products) {
189-
var session = deserialize(event, Session.class);
188+
var session = EventPayloads.deserialize(event, Session.class);
190189
String sessionId;
191190
if (session != null) {
192191
sessionId = session.getId();
@@ -214,7 +213,7 @@ private void handleSessionExpired(Event event, StripeEventContext ctx, ProductsC
214213
}
215214

216215
private void handleChargeRefunded(Event event, StripeEventContext ctx, ProductsConfig products) {
217-
var charge = deserialize(event, Charge.class);
216+
var charge = EventPayloads.deserialize(event, Charge.class);
218217
String paymentIntentId;
219218
Long refundAmount;
220219
String currency;
@@ -270,7 +269,7 @@ private void handleChargeRefunded(Event event, StripeEventContext ctx, ProductsC
270269
}
271270

272271
private void handleDisputeCreated(Event event, StripeEventContext ctx, ProductsConfig products) {
273-
var dispute = deserialize(event, Dispute.class);
272+
var dispute = EventPayloads.deserialize(event, Dispute.class);
274273
String paymentIntentId;
275274
Long disputeAmount;
276275
String currency;
@@ -546,16 +545,6 @@ private MongoCollection<BsonDocument> transactionsCollection(StripeEventContext
546545
.getCollection(products.transactionsCollection(), BsonDocument.class);
547546
}
548547

549-
@SuppressWarnings("unchecked")
550-
private static <T extends StripeObject> T deserialize(Event event, Class<T> type) {
551-
var obj = event.getDataObjectDeserializer().getObject();
552-
if (obj.isEmpty() || !type.isInstance(obj.get())) {
553-
LOGGER.warn("[stripe] could not deserialize {} payload as {} (API version mismatch?)",
554-
event.getType(), type.getSimpleName());
555-
return null;
556-
}
557-
return (T) obj.get();
558-
}
559548

560549
// ── Notifications ────────────────────────────────────────────────────────
561550

stripe/src/main/java/org/restheart/stripe/webhook/SubscriptionEventHandler.java

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343

4444
import com.stripe.model.Event;
4545
import com.stripe.model.Invoice;
46-
import com.stripe.model.StripeObject;
4746
import com.stripe.model.Subscription;
4847
import com.stripe.model.SubscriptionItem;
4948
import com.stripe.model.checkout.Session;
@@ -95,7 +94,7 @@ public void handle(Event event, StripeEventContext ctx) throws Exception {
9594
// ── Handlers ─────────────────────────────────────────────────────────────
9695

9796
private void handleCheckoutSessionCompleted(Event event) {
98-
var session = deserialize(event, Session.class);
97+
var session = EventPayloads.deserialize(event, Session.class);
9998
if (session == null) {
10099
return;
101100
}
@@ -104,7 +103,7 @@ private void handleCheckoutSessionCompleted(Event event) {
104103
}
105104

106105
private void handleSubscriptionUpsert(Event event, StripeEventContext ctx) {
107-
var subscription = deserialize(event, Subscription.class);
106+
var subscription = EventPayloads.deserialize(event, Subscription.class);
108107
if (subscription == null) {
109108
return;
110109
}
@@ -141,7 +140,7 @@ private void notifyOverLimit(StripeEventContext ctx, SubscriptionOwner owner, Su
141140
}
142141

143142
private void handleSubscriptionDeleted(Event event, StripeEventContext ctx) {
144-
var subscription = deserialize(event, Subscription.class);
143+
var subscription = EventPayloads.deserialize(event, Subscription.class);
145144
if (subscription == null) {
146145
return;
147146
}
@@ -173,7 +172,7 @@ private void handleSubscriptionDeleted(Event event, StripeEventContext ctx) {
173172
}
174173

175174
private void handleTrialWillEnd(Event event, StripeEventContext ctx) {
176-
var subscription = deserialize(event, Subscription.class);
175+
var subscription = EventPayloads.deserialize(event, Subscription.class);
177176
if (subscription == null) {
178177
return;
179178
}
@@ -200,7 +199,7 @@ private void handleTrialWillEnd(Event event, StripeEventContext ctx) {
200199
}
201200

202201
private void handleInvoicePaymentSucceeded(Event event, StripeEventContext ctx) {
203-
var invoice = deserialize(event, Invoice.class);
202+
var invoice = EventPayloads.deserialize(event, Invoice.class);
204203
if (invoice == null || !hasSubscription(invoice)) {
205204
return;
206205
}
@@ -216,7 +215,7 @@ private void handleInvoicePaymentSucceeded(Event event, StripeEventContext ctx)
216215
}
217216

218217
private void handleInvoicePaymentFailed(Event event, StripeEventContext ctx) {
219-
var invoice = deserialize(event, Invoice.class);
218+
var invoice = EventPayloads.deserialize(event, Invoice.class);
220219
if (invoice == null || !hasSubscription(invoice)) {
221220
return;
222221
}
@@ -297,14 +296,4 @@ private static boolean hasSubscription(Invoice invoice) {
297296
&& invoice.getParent().getSubscriptionDetails().getSubscription() != null;
298297
}
299298

300-
@SuppressWarnings("unchecked")
301-
private static <T extends StripeObject> T deserialize(Event event, Class<T> type) {
302-
var obj = event.getDataObjectDeserializer().getObject();
303-
if (obj.isEmpty() || !type.isInstance(obj.get())) {
304-
LOGGER.warn("[stripe] could not deserialize {} payload as {} (API version mismatch?)",
305-
event.getType(), type.getSimpleName());
306-
return null;
307-
}
308-
return (T) obj.get();
309-
}
310299
}

0 commit comments

Comments
 (0)