Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.stripe;

import com.stripe.model.v2.core.EventNotification;

/** Functional interface for a hook that runs before any event-specific callback. */
@FunctionalInterface
public interface EventNotificationPreHandleCallback {
// this is an internal-facing method name that dictates how we call the stored method
boolean process(EventNotification event, StripeClient client);
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ abstract class StripeEventNotificationHandlerBase<T extends StripeEventNotificat

final StripeClient client;
private final EventNotificationFallbackCallback fallbackCallback;
private EventNotificationPreHandleCallback preHandleCallback;
private final HashMap<String, EventNotificationCallback<? extends EventNotification>>
registeredHandlers = new HashMap<>();

Expand All @@ -65,18 +66,50 @@ abstract class StripeEventNotificationHandlerBase<T extends StripeEventNotificat
this.fallbackCallback = fallbackCallback;
}

private <E extends EventNotification> void register(
String eventType, EventNotificationCallback<E> handler) {
/**
* Callbacks are expected to be registered once on startup, so registering anything after handling
* has begun indicates a bug.
*/
private void assertCanRegister() {
if (hasHandledEvent) {
throw new IllegalStateException("Cannot register handlers after handling an event");
throw new IllegalStateException(
"Cannot register new callbacks after an event has been handled. This is indicative of a bug.");
}
}

private <E extends EventNotification> void register(
String eventType, EventNotificationCallback<E> handler) {
assertCanRegister();

if (this.registeredHandlers.containsKey(eventType)) {
throw new IllegalArgumentException("Handler already registered for event type: " + eventType);
throw new IllegalArgumentException(
"Callback for event type \"" + eventType + "\" is already registered");
}
this.registeredHandlers.put(eventType, handler);
}

/**
* Registers a function that will be run before any event-specific callbacks. A useful place to
* store event-agnostic logic, such as logging or checking for <a
* href="https://docs.stripe.com/webhooks#handle-duplicate-events">duplicate event deliveries</a>.
*
* <p>Returning {@code true} causes handling to continue as normal; returning {@code false}
* returns from {@code handle()} immediately, so neither the registered callback nor the fallback
* callback are called.
*
* @param callback the hook to run before handling continues
* @return this handler, for chaining
*/
public T preHandle(EventNotificationPreHandleCallback callback) {
assertCanRegister();

if (this.preHandleCallback != null) {
throw new IllegalArgumentException("A preHandle callback is already registered");
}
this.preHandleCallback = callback;
return self();
}

/** Lets the generated {@code on*} methods return the concrete handler type for chaining. */
@SuppressWarnings("unchecked")
final T self() {
Expand All @@ -91,6 +124,11 @@ void dispatch(EventNotification eventNotification) {
// Create a new client with the event's context for thread-safe processing
StripeClient eventClient = this.client.withStripeContext(eventNotification.context);

if (this.preHandleCallback != null
&& !this.preHandleCallback.process(eventNotification, eventClient)) {
return;
}

if (handler == null) {
boolean isKnownEventType =
!(eventNotification instanceof com.stripe.events.UnknownEventNotification);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

/**
* Receive and process event notifications (AKA thin events) like
Expand All @@ -28,6 +31,7 @@
* <li>create a StripeClient called client
* <li>Initialize an EventNotificationHandler with the client, webhook secret, and fallback
* callback
* <li>register a preHandle hook that deduplicates events by id before any callback runs
* <li>register a specific handler for the "v1.billing.meter.error_report_triggered" event
* notification type
* <li>use handler.handle() to process the received notification webhook body
Expand All @@ -42,6 +46,12 @@ public class EventNotificationHandlerEndpoint {
private static final String API_KEY = System.getenv("STRIPE_API_KEY");
private static final String WEBHOOK_SECRET = System.getenv("WEBHOOK_SECRET");

// Webhooks can be delivered more than once, so we track ids we've already processed. In
// production, back this with something durable and shared across processes (e.g. Redis or a
// database table) instead of an in-memory Set.
private static final Set<String> processedEventIds =
Collections.synchronizedSet(new LinkedHashSet<>());

private static final StripeClient client = new StripeClient(API_KEY);
private static final StripeEventNotificationHandler handler =
client.notificationHandler(
Expand All @@ -54,8 +64,13 @@ public class EventNotificationHandlerEndpoint {
EventNotificationHandlerEndpoint::fallbackCallback);

public static void main(String[] args) throws IOException {
handler.preHandle(EventNotificationHandlerEndpoint::deduplicate);
// can be anywhere in your codebase; registering on both handlers means either endpoint below
// will route this event type
handler.onV1BillingMeterErrorReportTriggered(
EventNotificationHandlerEndpoint::handleMeterErrors);

unverifiedHandler.preHandle(EventNotificationHandlerEndpoint::deduplicate);
unverifiedHandler.onV1BillingMeterErrorReportTriggered(
EventNotificationHandlerEndpoint::handleMeterErrors);

Expand All @@ -71,6 +86,18 @@ private static void fallbackCallback(
System.out.println("Received unhandled event notification type: " + notif.getType());
}

/**
* Runs before any registered callback. Returning {@code false} here skips handling entirely for
* this delivery, which is useful for deduplicating webhooks.
*/
private static boolean deduplicate(EventNotification notif, StripeClient client) {
boolean isNewEvent = processedEventIds.add(notif.getId());
if (!isNewEvent) {
System.out.println("Skipping already-processed event: " + notif.getId());
}
return isNewEvent;
}

private static void handleMeterErrors(
V1BillingMeterErrorReportTriggeredEventNotification notif, StripeClient client) {
Meter meter;
Expand Down
Loading
Loading