diff --git a/src/main/java/com/imgfloat/app/config/TwitchOAuth2ErrorResponseErrorHandler.java b/src/main/java/com/imgfloat/app/config/TwitchOAuth2ErrorResponseErrorHandler.java index cb0f3f8..74ad11b 100644 --- a/src/main/java/com/imgfloat/app/config/TwitchOAuth2ErrorResponseErrorHandler.java +++ b/src/main/java/com/imgfloat/app/config/TwitchOAuth2ErrorResponseErrorHandler.java @@ -1,6 +1,7 @@ package com.imgfloat.app.config; import java.io.IOException; +import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import org.slf4j.Logger; @@ -26,23 +27,72 @@ class TwitchOAuth2ErrorResponseErrorHandler extends OAuth2ErrorResponseErrorHand @Override public void handleError(ClientHttpResponse response) throws IOException { + byte[] bodyBytes = StreamUtils.copyToByteArray(response.getBody()); + String body = new String(bodyBytes, StandardCharsets.UTF_8); + + if (body.isBlank()) { + LOG.warn("Failed to parse Twitch OAuth error response (status: {}, headers: {}): ", + response.getStatusCode(), + response.getHeaders()); + throw asAuthorizationException(body, null); + } + try { - super.handleError(response); - } catch (HttpMessageNotReadableException ex) { - String body = StreamUtils.copyToString(response.getBody(), StandardCharsets.UTF_8); + super.handleError(new CachedBodyClientHttpResponse(response, bodyBytes)); + } catch (HttpMessageNotReadableException | IllegalArgumentException ex) { LOG.warn("Failed to parse Twitch OAuth error response (status: {}, headers: {}): {}", response.getStatusCode(), response.getHeaders(), - body.isBlank() ? "" : body, + body, ex); throw asAuthorizationException(body, ex); } } - private OAuth2AuthorizationException asAuthorizationException(String body, - HttpMessageNotReadableException ex) { + private OAuth2AuthorizationException asAuthorizationException(String body, Exception ex) { String description = "Failed to parse Twitch OAuth error response" + (body.isBlank() ? "." : ": " + body); OAuth2Error oauth2Error = new OAuth2Error("invalid_token_response", description, null); return new OAuth2AuthorizationException(oauth2Error, ex); } + + private static final class CachedBodyClientHttpResponse implements ClientHttpResponse { + + private final ClientHttpResponse delegate; + private final byte[] body; + + private CachedBodyClientHttpResponse(ClientHttpResponse delegate, byte[] body) { + this.delegate = delegate; + this.body = body; + } + + @Override + public org.springframework.http.HttpStatusCode getStatusCode() throws IOException { + return delegate.getStatusCode(); + } + + @Override + public int getRawStatusCode() throws IOException { + return delegate.getRawStatusCode(); + } + + @Override + public String getStatusText() throws IOException { + return delegate.getStatusText(); + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public java.io.InputStream getBody() throws IOException { + return new ByteArrayInputStream(body); + } + + @Override + public org.springframework.http.HttpHeaders getHeaders() { + return delegate.getHeaders(); + } + } } diff --git a/src/test/java/com/imgfloat/app/config/TwitchOAuth2ErrorResponseErrorHandlerTest.java b/src/test/java/com/imgfloat/app/config/TwitchOAuth2ErrorResponseErrorHandlerTest.java new file mode 100644 index 0000000..f9a84de --- /dev/null +++ b/src/test/java/com/imgfloat/app/config/TwitchOAuth2ErrorResponseErrorHandlerTest.java @@ -0,0 +1,58 @@ +package com.imgfloat.app.config; + +import java.net.URI; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.http.client.MockClientHttpResponse; +import org.springframework.security.oauth2.core.OAuth2AuthorizationException; +import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +class TwitchOAuth2ErrorResponseErrorHandlerTest { + + private final TwitchOAuth2ErrorResponseErrorHandler handler = new TwitchOAuth2ErrorResponseErrorHandler(); + + @Test + void fallsBackToSyntheticErrorWhenErrorBodyIsMissing() throws Exception { + MockClientHttpResponse response = new MockClientHttpResponse(new byte[0], HttpStatus.BAD_REQUEST); + response.getHeaders().setContentType(MediaType.APPLICATION_JSON); + + OAuth2AuthorizationException exception = assertThrows(OAuth2AuthorizationException.class, + () -> handler.handleError(response)); + + assertThat(exception.getError().getErrorCode()).isEqualTo("invalid_token_response"); + assertThat(exception.getError().getDescription()) + .contains("Failed to parse Twitch OAuth error response"); + } + + @Test + void successfulResponsesStillParseNormally() { + RestTemplate restTemplate = OAuth2RestTemplateFactory.create(); + restTemplate.setErrorHandler(new TwitchOAuth2ErrorResponseErrorHandler()); + MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build(); + + server.expect(requestTo("https://id.twitch.tv/oauth2/token")) + .andRespond(withSuccess( + "{\"access_token\":\"abc\",\"token_type\":\"bearer\",\"expires_in\":3600,\"scope\":[]}", + MediaType.APPLICATION_JSON)); + + RequestEntity request = RequestEntity.post(URI.create("https://id.twitch.tv/oauth2/token")).build(); + ResponseEntity response = restTemplate.exchange(request, OAuth2AccessTokenResponse.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getAccessToken().getTokenValue()).isEqualTo("abc"); + + server.verify(); + } +}