diff --git a/java/core/src/main/java/co/worklytics/psoxy/gateway/ProxyConfigProperty.java b/java/core/src/main/java/co/worklytics/psoxy/gateway/ProxyConfigProperty.java index bd46c2861..58f225c90 100644 --- a/java/core/src/main/java/co/worklytics/psoxy/gateway/ProxyConfigProperty.java +++ b/java/core/src/main/java/co/worklytics/psoxy/gateway/ProxyConfigProperty.java @@ -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). */ diff --git a/java/core/src/main/java/co/worklytics/psoxy/gateway/impl/ApiDataRequestHandler.java b/java/core/src/main/java/co/worklytics/psoxy/gateway/impl/ApiDataRequestHandler.java index c1b638d29..44db61b5b 100644 --- a/java/core/src/main/java/co/worklytics/psoxy/gateway/impl/ApiDataRequestHandler.java +++ b/java/core/src/main/java/co/worklytics/psoxy/gateway/impl/ApiDataRequestHandler.java @@ -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) diff --git a/java/core/src/test/java/co/worklytics/psoxy/gateway/impl/ApiDataRequestHandlerTest.java b/java/core/src/test/java/co/worklytics/psoxy/gateway/impl/ApiDataRequestHandlerTest.java index 476fae358..40dcf42b8 100644 --- a/java/core/src/test/java/co/worklytics/psoxy/gateway/impl/ApiDataRequestHandlerTest.java +++ b/java/core/src/test/java/co/worklytics/psoxy/gateway/impl/ApiDataRequestHandlerTest.java @@ -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() {