Skip to content
Draft
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
Expand Up @@ -142,12 +142,11 @@ public enum ProxyConfigProperty implements ConfigService.ConfigProperty {
/**
* Whether the proxy should follow HTTP redirects (3xx responses) when calling source APIs.
*
* OPTIONAL; defaults to {@code true} (redirects are followed), matching the default behavior
* of the underlying HTTP client.
* OPTIONAL. When unset, redirects are followed in synchronous processing and disabled in
* async processing so the proxy can intercept 3xx responses and fetch from the Location URL
* manually (required for connectors such as ChatGPT Enterprise and Slack Analytics).
*
* Set to {@code FALSE} for connectors whose APIs issue redirects that must not be
* automatically followed — e.g. ChatGPT Enterprise, which uses async processing and relies
* on the proxy intercepting 3xx responses to fetch data from the Location URL manually.
* Set explicitly to override that default for a connector instance.
*
* Accepted values: {@code true} / {@code false} (case-insensitive).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ public HttpEventResponse handle(HttpEventRequest requestToProxy,
// setup request
boolean followRedirects = config.getConfigPropertyAsOptional(ProxyConfigProperty.FOLLOW_REDIRECTS)
.map(Boolean::parseBoolean)
.orElse(true);
.orElse(!processingContext.getAsync());

requestToSourceApi
.setThrowExceptionOnExecuteError(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,62 @@ void isRedirectFamily() {
assertFalse(handler.isRedirectFamily(400));
}

@Test
@SneakyThrows
void asyncModeDisablesRedirectFollowingByDefault() {
setup("gmail", "google.apis.com");

ApiDataRequestHandler spy = spy(handler);

HttpEventRequest request = MockModules.provideMock(HttpEventRequest.class);
when(request.getHeader(ControlHeader.PSEUDONYM_IMPLEMENTATION.getHttpHeader()))
.thenReturn(Optional.empty());
when(request.getHttpMethod()).thenReturn("GET");
when(request.getPath()).thenReturn("/admin/directory/v1/users");
when(request.getQuery()).thenReturn(Optional.empty());

HttpRequest[] captured = new HttpRequest[1];
MockHttpTransport transport = new MockHttpTransport();
HttpRequestFactory requestFactory = spy(transport.createRequestFactory());
doAnswer(invocation -> {
HttpRequest built = (HttpRequest) invocation.callRealMethod();
captured[0] = built;
return built;
}).when(requestFactory).buildRequest(anyString(), any(GenericUrl.class), any());
doReturn(requestFactory).when(spy).getRequestFactory(any());

MockHttpTransport contentTransport = new MockHttpTransport() {
@Override
public LowLevelHttpRequest buildRequest(String method, String url) throws IOException {
return new MockLowLevelHttpRequest() {
@Override
public LowLevelHttpResponse execute() throws IOException {
MockLowLevelHttpResponse response = new MockLowLevelHttpResponse();
response.setStatusCode(200);
response.setContentType(Json.MEDIA_TYPE);
response.setContent("[]");
return response;
}
};
}
};
when(spy.httpTransportFactory.create()).thenReturn(contentTransport);

RESTApiSanitizerImpl sanitizer = mock(RESTApiSanitizerImpl.class);
when(sanitizer.isAllowed(anyString(), any(), anyString(), any())).thenReturn(true);
spy.sanitizer = sanitizer;

spy.handle(request, ApiDataRequestHandler.ProcessingContext.builder()
.async(true)
.requestId("r")
.asyncOutputLocation("gs://bucket/output.json")
.requestReceivedAt(clock.instant())
.build());

assertNotNull(captured[0]);
assertFalse(captured[0].getFollowRedirects());
}

@Test
@SneakyThrows
void handleShouldFollowRedirectManuallyInAsyncMode() {
Expand Down
Loading