Skip to content
Open
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,6 @@
---
title: Preserve event context and clear inherited account scoping when fetching and mutating related objects
semver_level: patch
jira_tickets_closed:
- RUN_DEVSDK-3694
---
56 changes: 10 additions & 46 deletions src/main/java/com/stripe/StripeClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ protected StripeResponseGetter getResponseGetter() {

/** Gets the current StripeContext from the client's configuration. Used in unit testing. */
protected String getContext() {
if (responseGetter instanceof ContextScopedStripeResponseGetter) {
return ((ContextScopedStripeResponseGetter) responseGetter).getStripeContext();
}

// TODO(major): add getOptions to the StripeResponseGetter interface? that would simplify this
if (!(responseGetter instanceof LiveStripeResponseGetter)) {
return null;
Expand All @@ -65,56 +69,16 @@ protected String getContext() {
* and HTTP client as this client, but will have the specified context. This allows for efficient
* parallel processing without reinitializing HTTP connections.
*
* <p>The default stripe_account is cleared. Explicit per-request context and account options
* still take precedence over these defaults.
*
* @param context the custom stripe_context to use for the new client
* @return a new StripeClient with the custom context
* @throws IllegalStateException if this client doesn't use a LiveStripeResponseGetter
*/
public StripeClient withStripeContext(StripeContext context) {
// Convert StripeContext to String
String contextString = (context == null) ? null : context.toString();

StripeResponseGetter responseGetter = this.getResponseGetter();

// We can only create a new client for LiveStripeResponseGetter because it's the only class with
// `getOptions()`. If we add that method to the interface in a later major, we could remove this
// check.
if (!(responseGetter instanceof LiveStripeResponseGetter)) {
throw new IllegalStateException(
"Cannot create a client with custom context for non-Live response getters");
}

LiveStripeResponseGetter liveGetter = (LiveStripeResponseGetter) responseGetter;

// Create a new LiveStripeResponseGetter with updated context, reusing the HTTP client
LiveStripeResponseGetter newResponseGetter =
liveGetter.withNewOptions(
options -> {
if (options instanceof ClientStripeResponseGetterOptions) {
return ((ClientStripeResponseGetterOptions) options)
.toBuilder()
.stripeAccount(null)
.stripeContext(contextString)
.build();
}

return new ClientStripeResponseGetterOptions(
options.getAuthenticator(),
options.getClientId(),
options.getConnectTimeout(),
options.getReadTimeout(),
options.getMaxNetworkRetries(),
options.getConnectionProxy(),
options.getProxyCredential(),
options.getApiBase(),
options.getFilesBase(),
options.getConnectBase(),
options.getMeterEventsBase(),
null,
contextString);
});

// Create and return a new StripeClient with the new response getter
return new StripeClient(newResponseGetter);
return new StripeClient(
ContextScopedStripeResponseGetter.withStripeContext(
this.getResponseGetter(), context == null ? null : context.toString()));
}

/**
Expand Down
7 changes: 6 additions & 1 deletion src/main/java/com/stripe/model/v2/core/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.stripe.net.ApiRequest;
import com.stripe.net.ApiResource;
import com.stripe.net.BaseAddress;
import com.stripe.net.ContextScopedStripeResponseGetter;
import com.stripe.net.RequestOptions;
import com.stripe.net.StripeResponseGetter;
import java.time.Instant;
Expand Down Expand Up @@ -126,7 +127,11 @@ private StripeObject fetchRelatedObjectByTypeAndUrl(String type, String url)

RequestOptions opts = optsBuilder.build();

return this.responseGetter.request(
// The related object must retain the event's scope for subsequent requests.
StripeResponseGetter scopedResponseGetter =
ContextScopedStripeResponseGetter.withStripeContext(this.responseGetter, context);

return scopedResponseGetter.request(
new ApiRequest(BaseAddress.API, ApiResource.RequestMethod.GET, url, null, opts),
objectClass);
}
Expand Down
12 changes: 6 additions & 6 deletions src/main/java/com/stripe/model/v2/core/EventNotification.java
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,15 @@ private RawRequestOptions getRequestOptions() {
protected Event fetchEvent() throws StripeException {
// `id` comes from the notification body, so encode it the way the generated
// services do -- otherwise it can inject extra path or query segments.
StripeClient scopedClient = client.withStripeContext(context);
StripeResponse response =
client.rawRequest(
scopedClient.rawRequest(
RequestMethod.GET,
String.format("/v2/core/events/%s", ApiResource.urlEncodeId(id)),
null,
getRequestOptions());

return (Event) client.withStripeContext(context).deserialize(response.body(), ApiMode.V2);
return (Event) scopedClient.deserialize(response.body(), ApiMode.V2);
}

/** Retrieves the object associated with the event. */
Expand All @@ -176,11 +177,10 @@ protected StripeObject fetchRelatedObject(RelatedSingletonObject relatedObject)
}

private StripeObject fetchRelatedObjectByUrl(String relativeUrl) throws StripeException {
StripeClient scopedClient = client.withStripeContext(context);
StripeResponse response =
client.rawRequest(RequestMethod.GET, relativeUrl, null, getRequestOptions());
scopedClient.rawRequest(RequestMethod.GET, relativeUrl, null, getRequestOptions());

return client
.withStripeContext(context)
.deserialize(response.body(), ApiMode.getMode(relativeUrl));
return scopedClient.deserialize(response.body(), ApiMode.getMode(relativeUrl));
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/stripe/net/ApiRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ public ApiRequest(
this(baseAddress, method, path, options, null, params);
}

ApiRequest withOptions(RequestOptions options) {
return new ApiRequest(
this.getBaseAddress(),
this.getMethod(),
this.getPath(),
options,
this.getUsage(),
this.getParams());
}

public ApiRequest addUsage(String usage) {
List<String> newUsage = new ArrayList<>();
if (this.getUsage() != null) {
Expand Down
116 changes: 116 additions & 0 deletions src/main/java/com/stripe/net/ContextScopedStripeResponseGetter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.stripe.net;

import com.stripe.exception.StripeException;
import com.stripe.model.StripeActiveObject;
import com.stripe.model.StripeObject;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.Map;

/**
* Applies a default context and suppresses inherited account scoping without replacing the
* underlying requestor. Explicit request options retain their usual precedence.
*
* <p>For internal SDK use.
*/
public final class ContextScopedStripeResponseGetter implements StripeResponseGetter {
private final StripeResponseGetter delegate;
private final String stripeContext;

private ContextScopedStripeResponseGetter(StripeResponseGetter delegate, String stripeContext) {
this.delegate = delegate;
this.stripeContext = stripeContext;
}

/**
* Creates a getter with replacement context/account defaults, retaining the original requestor.
*/
public static ContextScopedStripeResponseGetter withStripeContext(
StripeResponseGetter delegate, String stripeContext) {
if (delegate instanceof ContextScopedStripeResponseGetter) {
delegate = ((ContextScopedStripeResponseGetter) delegate).delegate;
}
return new ContextScopedStripeResponseGetter(delegate, stripeContext);
}

/** Returns the scoped default context, including null when the context is cleared. */
public String getStripeContext() {
return stripeContext;
}

private <T extends RequestOptions.RequestOptionsBuilder> T scope(T builder) {
if (builder.getStripeContext() == null) {
// An empty context explicitly clears the delegate's default context during merging.
builder.setStripeContext(stripeContext == null ? "" : stripeContext);
}
builder.stripeAccountSuppressed = true;
return builder;
}

private RequestOptions scope(RequestOptions options) {
return scope(options == null ? RequestOptions.builder() : options.toBuilderFullCopy()).build();
}

private RawRequestOptions scope(RawRequestOptions options) {
return scope(options == null ? RawRequestOptions.builder() : options.toBuilderFullCopy())
.build();
}

private <T extends StripeObject> T retainScope(T resource) {
if (resource instanceof StripeActiveObject) {
((StripeActiveObject) resource).setResponseGetter(this);
}
return resource;
}

@Override
@SuppressWarnings("TypeParameterUnusedInFormals")
public <T extends StripeObject> T request(ApiRequest request, Type typeToken)
throws StripeException {
return retainScope(
delegate.request(request.withOptions(scope(request.getOptions())), typeToken));
}

@Override
public InputStream requestStream(ApiRequest request) throws StripeException {
return delegate.requestStream(request.withOptions(scope(request.getOptions())));
}

@Override
public StripeResponse rawRequest(RawApiRequest request) throws StripeException {
return delegate.rawRequest(request.withOptions(scope(request.getOptions())));
}

@Override
@SuppressWarnings({"TypeParameterUnusedInFormals", "deprecation"})
public <T extends StripeObject> T request(
BaseAddress baseAddress,
ApiResource.RequestMethod method,
String path,
Map<String, Object> params,
Type typeToken,
RequestOptions options,
ApiMode apiMode)
throws StripeException {
return retainScope(
delegate.request(baseAddress, method, path, params, typeToken, scope(options), apiMode));
}

@Override
@SuppressWarnings("deprecation")
public InputStream requestStream(
BaseAddress baseAddress,
ApiResource.RequestMethod method,
String path,
Map<String, Object> params,
RequestOptions options,
ApiMode apiMode)
throws StripeException {
return delegate.requestStream(baseAddress, method, path, params, scope(options), apiMode);
}

@Override
public void validateRequestOptions(RequestOptions options) {
delegate.validateRequestOptions(scope(options));
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/stripe/net/RawApiRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ public RawApiRequest(
this(baseAddress, method, path, options, null, rawContent);
}

RawApiRequest withOptions(RawRequestOptions options) {
return new RawApiRequest(
this.getBaseAddress(),
this.getMethod(),
this.getPath(),
options,
this.getUsage(),
this.getRawContent());
}

public RawApiRequest addUsage(String usage) {
List<String> newUsage = new ArrayList<>();
if (this.getUsage() != null) {
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/com/stripe/net/RawRequestOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public Map<String, String> getAdditionalHeaders() {
return additionalHeaders;
}

@Override
public RawRequestOptionsBuilder toBuilderFullCopy() {
return copyToBuilder(new RawRequestOptionsBuilder()).setAdditionalHeaders(additionalHeaders);
}

public static RawRequestOptionsBuilder builder() {
return new RawRequestOptionsBuilder();
}
Expand Down
27 changes: 21 additions & 6 deletions src/main/java/com/stripe/net/RequestOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class RequestOptions {
private final String stripeRequestTrigger;
private final String idempotencyKey;
private final String stripeAccount;
// A scoped getter must distinguish suppressing an inherited account from leaving it unspecified.
private final boolean stripeAccountSuppressed;
private final String baseUrl;
/** Stripe version always set at {@link Stripe#API_VERSION}. */
private final String stripeVersion = Stripe.API_VERSION;
Expand All @@ -42,6 +44,7 @@ private RequestOptions(
String stripeContext,
String stripeRequestTrigger,
String stripeAccount,
boolean stripeAccountSuppressed,
String stripeVersionOverride,
String baseUrl,
Integer connectTimeout,
Expand All @@ -55,6 +58,7 @@ private RequestOptions(
this.stripeContext = stripeContext;
this.stripeRequestTrigger = stripeRequestTrigger;
this.stripeAccount = stripeAccount;
this.stripeAccountSuppressed = stripeAccountSuppressed;
this.stripeVersionOverride = stripeVersionOverride;
this.baseUrl = baseUrl;
this.connectTimeout = connectTimeout;
Expand All @@ -72,6 +76,7 @@ private RequestOptions(
builder.stripeContext,
builder.stripeRequestTrigger,
normalizeStripeAccount(builder.stripeAccount),
builder.stripeAccountSuppressed,
normalizeStripeVersion(builder.stripeVersionOverride),
normalizeBaseUrl(builder.baseUrl),
builder.connectTimeout,
Expand Down Expand Up @@ -161,8 +166,14 @@ public RequestOptionsBuilder toBuilder() {
* @return option builder.
*/
public RequestOptionsBuilder toBuilderFullCopy() {
return RequestOptionsBuilder.unsafeSetStripeVersionOverride(
new RequestOptionsBuilder()
return copyToBuilder(new RequestOptionsBuilder());
}

/** Copies all options, including internal scoping state, into the supplied builder. */
protected <T extends RequestOptionsBuilder> T copyToBuilder(T builder) {
builder.stripeAccountSuppressed = this.stripeAccountSuppressed;
RequestOptionsBuilder.unsafeSetStripeVersionOverride(
builder
.setAuthenticator(this.authenticator)
.setBaseUrl(this.baseUrl)
.setClientId(this.clientId)
Expand All @@ -176,6 +187,7 @@ public RequestOptionsBuilder toBuilderFullCopy() {
.setConnectionProxy(this.connectionProxy)
.setProxyCredential(this.proxyCredential),
stripeVersionOverride);
return builder;
}

public static class RequestOptionsBuilder {
Expand All @@ -185,6 +197,7 @@ public static class RequestOptionsBuilder {
protected String stripeContext;
protected String stripeRequestTrigger;
protected String stripeAccount;
boolean stripeAccountSuppressed;
protected String stripeVersionOverride;
protected Integer connectTimeout;
protected Integer readTimeout;
Expand Down Expand Up @@ -521,6 +534,11 @@ static RequestOptions merge(StripeResponseGetterOptions clientOptions, RequestOp
stripeContext = clientOptions.getStripeContext();
}

String stripeAccount = options.getStripeAccount();
if (stripeAccount == null && !options.stripeAccountSuppressed) {
stripeAccount = clientOptions.getStripeAccount();
}

return RequestOptionsBuilder.unsafeSetStripeVersionOverride(
new RequestOptionsBuilder()
.setAuthenticator(
Expand All @@ -534,10 +552,7 @@ static RequestOptions merge(StripeResponseGetterOptions clientOptions, RequestOp
.setIdempotencyKey(options.getIdempotencyKey())
.setStripeContext(stripeContext)
.setStripeRequestTrigger(options.getStripeRequestTrigger())
.setStripeAccount(
options.getStripeAccount() != null
? options.getStripeAccount()
: clientOptions.getStripeAccount())
.setStripeAccount(stripeAccount)
.setConnectTimeout(
options.getConnectTimeout() != null
? options.getConnectTimeout()
Expand Down
Loading
Loading