Implemented an ExchangeFilterFunction, to support Spring WebFlux facebook-errors
authorKai Moritz <kai@jupl.de>
Wed, 20 Nov 2019 15:02:48 +0000 (16:02 +0100)
committerKai Moritz <kai@jupl.de>
Fri, 22 Nov 2019 06:45:31 +0000 (07:45 +0100)
* Guckst du hier: https://stackoverflow.com/a/48984852/247276

pom.xml
src/main/java/de/juplo/facebook/errors/GraphApiExchangeFilterFunction.java [new file with mode: 0644]
src/test/java/de/juplo/facebook/errors/GraphApiExchangeFilterFunctionIntegrationTest.java [new file with mode: 0644]

diff --git a/pom.xml b/pom.xml
index b1fc524..073e90f 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -70,7 +70,8 @@
     </dependency>
     <dependency>
       <groupId>org.springframework</groupId>
-      <artifactId>spring-webmvc</artifactId>
+      <artifactId>spring-webflux</artifactId>
+      <optional>true</optional>
     </dependency>
     <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>mockwebserver</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>io.projectreactor</groupId>
+      <artifactId>reactor-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-reactive-httpclient</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>javax.servlet</groupId>
       <artifactId>javax.servlet-api</artifactId>
diff --git a/src/main/java/de/juplo/facebook/errors/GraphApiExchangeFilterFunction.java b/src/main/java/de/juplo/facebook/errors/GraphApiExchangeFilterFunction.java
new file mode 100644 (file)
index 0000000..34ca5a7
--- /dev/null
@@ -0,0 +1,49 @@
+package de.juplo.facebook.errors;
+
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.reactive.function.client.ClientRequest;
+import org.springframework.web.reactive.function.client.ClientResponse;
+import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
+import org.springframework.web.reactive.function.client.ExchangeFunction;
+import org.springframework.web.reactive.function.client.WebClientResponseException;
+import reactor.core.publisher.Mono;
+
+
+/**
+ * An {@link ExchangeFilterFunction}
+ * @author Kai Moritz
+ */
+public class GraphApiExchangeFilterFunction implements ExchangeFilterFunction
+{
+  @Override
+  public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next)
+  {
+    return
+        next
+            .exchange(request)
+            .flatMap(response ->
+            {
+              return
+                  HttpStatus.Series.CLIENT_ERROR.equals(response.statusCode().series())
+                      ? response
+                          .bodyToMono(String.class)
+                          .map(errorBody -> GraphApiException.create(
+                              response.statusCode(),
+                              response.headers().asHttpHeaders(),
+                              errorBody.getBytes()))
+                          .map(e ->
+                              e.getClass().equals(ErrorResponseParsingErrorException.class)
+                                  ? WebClientResponseException
+                                      .create(
+                                          e.getStatus().value(),
+                                          e.getStatus().getReasonPhrase(),
+                                          e.getHeaders(),
+                                          null,
+                                          null)
+                                  : e)
+                          .flatMap(e -> Mono.error(e))
+                      : Mono.just(response);
+            });
+  }
+}
diff --git a/src/test/java/de/juplo/facebook/errors/GraphApiExchangeFilterFunctionIntegrationTest.java b/src/test/java/de/juplo/facebook/errors/GraphApiExchangeFilterFunctionIntegrationTest.java
new file mode 100644 (file)
index 0000000..53b2e6a
--- /dev/null
@@ -0,0 +1,271 @@
+package de.juplo.facebook.errors;
+
+
+import de.juplo.facebook.errors.GraphApiException.Type;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import org.junit.After;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.client.reactive.JettyClientHttpConnector;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.reactive.function.client.WebClientResponseException.BadRequest;
+import org.springframework.web.reactive.function.client.WebClientResponseException.InternalServerError;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+
+/**
+ *
+ * @author Kai Moritz
+ */
+public class GraphApiExchangeFilterFunctionIntegrationTest
+{
+  private static final Logger LOG =
+      LoggerFactory.getLogger(GraphApiExchangeFilterFunctionIntegrationTest.class);
+
+  private MockWebServer server;
+       private WebClient webClient;
+
+
+       @Before
+       public void setup()
+  {
+    server = new MockWebServer();
+    webClient =
+        WebClient
+            .builder()
+            .clientConnector(new JettyClientHttpConnector())
+            .baseUrl(server.url("/").toString())
+            .filter(new GraphApiExchangeFilterFunction())
+            .build();
+       }
+
+       @After
+       public void shutdown() throws Exception
+  {
+    this.server.shutdown();
+       }
+
+
+  @Test
+  public void testNoError()
+  {
+    Mono<String> result;
+
+
+    //server
+    //    .enqueue(new MockResponse()
+    //        .setResponseCode(HttpStatus.CONTINUE.value())
+    //        .setHeader("Content-Type", "application/json")
+    //        .setBody("Hallo Welt!"));
+    //
+    //result =
+    //    webClient
+    //        .get()
+    //        .uri("/egal")
+    //        .retrieve()
+    //        .bodyToMono(String.class);
+    //
+    //StepVerifier
+    //    .create(result)
+    //    .expectNext("Hallo Welt!")
+    //    .expectComplete()
+    //    .verify();
+
+
+    server
+        .enqueue(new MockResponse()
+            .setResponseCode(HttpStatus.OK.value())
+            .setHeader("Content-Type", "text/plain")
+            .setBody("Hallo Welt!"));
+
+    result =
+        webClient
+            .get()
+            .uri("/egal")
+            .retrieve()
+            .bodyToMono(String.class);
+
+    StepVerifier
+        .create(result)
+        .expectNext("Hallo Welt!")
+        .expectComplete()
+        .verify();
+
+
+    server
+        .enqueue(new MockResponse()
+            .setResponseCode(HttpStatus.TEMPORARY_REDIRECT.value())
+            .setHeader("Location", server.url("/woanders"))
+            .setHeader("Content-Type", "text/plain")
+            .setBody("Jetzt doch woanders..."));
+    server
+        .enqueue(new MockResponse()
+            .setResponseCode(HttpStatus.OK.value())
+            .setHeader("Content-Type", "text/plain")
+            .setBody("Hallo Welt!"));
+
+    result =
+        webClient
+            .get()
+            .uri("/egal")
+            .retrieve()
+            .bodyToMono(String.class);
+
+    StepVerifier
+        .create(result)
+        .expectNext("Hallo Welt!")
+        .expectComplete()
+        .verify();
+
+
+    server
+        .enqueue(new MockResponse()
+            .setResponseCode(HttpStatus.INTERNAL_SERVER_ERROR.value())
+            .setHeader("Content-Type", "text/plain")
+            .setBody("Hallo Welt!"));
+
+    result =
+        webClient
+            .get()
+            .uri("/egal")
+            .retrieve()
+            .bodyToMono(String.class);
+
+    StepVerifier
+        .create(result)
+        .expectError(InternalServerError.class)
+        .verify();
+  }
+
+  @Test
+  public void testValidError()
+  {
+    server
+        .enqueue(new MockResponse()
+            .setResponseCode(HttpStatus.BAD_REQUEST.value())
+            .setHeader("Content-Type", "application/json")
+            .setBody(
+                "{\n" +
+                "  \"error\":\n" +
+                "  {\n" +
+                "    \"message\": \"(#613) Calls to stream have exceeded the rate of 600 calls per 600 seconds.\",\n" +
+                "    \"type\": \"OAuthException\",\n" +
+                "    \"code\": 613\n" +
+                "  }\n" +
+                "}"));
+
+    Mono<String> result =
+        webClient
+            .get()
+            .uri("/egal")
+            .retrieve()
+            .bodyToMono(String.class);
+
+    StepVerifier.create(result).expectErrorSatisfies(throwable ->
+    {
+      assertEquals(RateLimitExceededException.class, throwable.getClass());
+      RateLimitExceededException e = (RateLimitExceededException)throwable;
+      LOG.debug("{}", e.toString());
+      assertEquals(new Integer(613), e.getCode());
+      assertEquals("(#613) Calls to stream have exceeded the rate of 600 calls per 600 seconds.", e.getMessage());
+      assertEquals(Type.OAuthException, e.getType());
+    })
+    .verify();
+  }
+
+  @Test
+  public void testUnmappedError()
+  {
+    server
+        .enqueue(new MockResponse()
+            .setResponseCode(HttpStatus.BAD_REQUEST.value())
+            .setHeader("Content-Type", "application/json")
+            .setBody(
+                "{\n" +
+                "  \"error\":\n" +
+                "  {\n" +
+                "    \"message\": \"This error does not exist.\",\n" +
+                "    \"type\": \"NonexistentTypeException\",\n" +
+                "    \"code\": 999999999\n" +
+                "  }\n" +
+                "}"));
+
+
+    Mono<String> result =
+        webClient
+            .get()
+            .uri("/egal")
+            .retrieve()
+            .bodyToMono(String.class);
+
+    StepVerifier.create(result).expectErrorSatisfies(throwable ->
+    {
+      assertEquals(UnmappedErrorException.class, throwable.getClass());
+      UnmappedErrorException e = (UnmappedErrorException)throwable;
+      LOG.debug("{}", e.toString());
+      assertEquals(new Integer(999999999), e.getCode());
+      assertEquals("This error does not exist.", e.getMessage());
+      try
+      {
+        Type type = e.getType();
+        LOG.error("unknown type: {}", type);
+        fail("unmapped type was resolved by enum: " + type);
+      }
+      catch (IllegalArgumentException ee) {}
+    })
+    .verify();
+  }
+
+  @Test
+  public void testInvlalidError()
+  {
+    Mono<String> result;
+
+
+    server
+        .enqueue(new MockResponse()
+            .setResponseCode(HttpStatus.BAD_REQUEST.value())
+            .setHeader("Content-Type", "application/json")
+            .setBody(
+                "{\n" +
+                "  \"error\":\n" +
+                "  {\n" +
+                "    \"message\": \"Not a Graph-Api-Exception.\",\n" +
+                "    \"type\": \"Whatever\",\n" +
+                "    \"code\": \"some string\"\n" +
+                "  }\n" +
+                "}"));
+
+    result =
+        webClient
+            .get()
+            .uri("/egal")
+            .retrieve()
+            .bodyToMono(String.class);
+
+    StepVerifier.create(result).expectError(BadRequest.class).verify();
+
+
+    server
+        .enqueue(new MockResponse()
+            .setResponseCode(HttpStatus.BAD_REQUEST.value())
+            .setHeader("Content-Type", "text/plain")
+            .setBody("Hallo Welt!"));
+
+    result =
+        webClient
+            .get()
+            .uri("/egal")
+            .retrieve()
+            .bodyToMono(String.class);
+
+    StepVerifier.create(result).expectError(BadRequest.class).verify();
+  }
+}