From d9d87df2218a9e0c110bda4e0b686557ccb699a4 Mon Sep 17 00:00:00 2001 From: Josiah Noel <32279667+SentryMan@users.noreply.github.com> Date: Wed, 28 Dec 2022 15:45:07 -0500 Subject: [PATCH 1/5] add support for Jsonb generics --- client/pom.xml | 10 ++-- .../io/avaje/http/client/BodyAdapter.java | 19 ++++++++ .../avaje/http/client/DHttpClientContext.java | 14 ++++++ .../avaje/http/client/DHttpClientRequest.java | 28 ++++++++++- .../avaje/http/client/HttpClientResponse.java | 47 +++++++++++++++++++ .../avaje/http/client/JsonbBodyAdapter.java | 30 ++++++++---- .../http/client/HelloControllerTest.java | 2 +- 7 files changed, 135 insertions(+), 15 deletions(-) diff --git a/client/pom.xml b/client/pom.xml index bf5c54f..cb42865 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -39,14 +39,14 @@ io.avaje avaje-jsonb - 1.0-RC1 + 1.1-RC2 true io.avaje avaje-inject - 8.6 + 8.10 true @@ -76,14 +76,14 @@ io.javalin javalin - 4.1.1 + 5.2.0 test io.avaje avaje-http-api - 1.16 + 1.20 test @@ -125,7 +125,7 @@ io.avaje avaje-inject-generator - 8.6 + 8.10 diff --git a/client/src/main/java/io/avaje/http/client/BodyAdapter.java b/client/src/main/java/io/avaje/http/client/BodyAdapter.java index 3c0ffa4..012e503 100644 --- a/client/src/main/java/io/avaje/http/client/BodyAdapter.java +++ b/client/src/main/java/io/avaje/http/client/BodyAdapter.java @@ -1,5 +1,6 @@ package io.avaje.http.client; +import java.lang.reflect.ParameterizedType; import java.util.List; /** @@ -23,6 +24,16 @@ public interface BodyAdapter { */ BodyReader beanReader(Class type); + /** + * Return a BodyReader to read response content and convert to a bean. + * + * @param type The bean type to convert the content to. + */ + default BodyReader beanReader(ParameterizedType type) { + throw new UnsupportedOperationException("Parameterized types not supported for this adapter"); + } + + /** * Return a BodyReader to read response content and convert to a list of beans. * @@ -30,4 +41,12 @@ public interface BodyAdapter { */ BodyReader> listReader(Class type); + /** + * Return a BodyReader to read response content and convert to a list of beans. + * + * @param type The bean type to convert the content to. + */ + default BodyReader> listReader(ParameterizedType type) { + throw new UnsupportedOperationException("Parameterized types not supported for this adapter"); + } } diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientContext.java b/client/src/main/java/io/avaje/http/client/DHttpClientContext.java index afa764f..d2c1064 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientContext.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientContext.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.lang.reflect.Constructor; +import java.lang.reflect.ParameterizedType; import java.net.http.HttpClient; import java.net.http.HttpHeaders; import java.net.http.HttpRequest; @@ -273,6 +274,10 @@ BodyReader beanReader(Class cls) { return bodyAdapter.beanReader(cls); } + BodyReader beanReader(ParameterizedType cls) { + return bodyAdapter.beanReader(cls); + } + T readBean(Class cls, BodyContent content) { return bodyAdapter.beanReader(cls).read(content); } @@ -281,6 +286,15 @@ List readList(Class cls, BodyContent content) { return bodyAdapter.listReader(cls).read(content); } + @SuppressWarnings("unchecked") + T readBean(ParameterizedType cls, BodyContent content) { + return (T) bodyAdapter.beanReader(cls).read(content); + } + + List readList(ParameterizedType cls, BodyContent content) { + return (List) bodyAdapter.listReader(cls).read(content); + } + void afterResponse(DHttpClientRequest request) { metricResTotal.add(1); metricResMicros.add(request.responseTimeMicros()); diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java index 8d7fed7..5fc3e6c 100644 --- a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java +++ b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java @@ -3,6 +3,7 @@ import javax.net.ssl.SSLSession; import java.io.FileNotFoundException; import java.io.InputStream; +import java.lang.reflect.ParameterizedType; import java.net.URI; import java.net.URLEncoder; import java.net.http.HttpClient; @@ -433,7 +434,7 @@ public List list(Class cls) { readResponseContent(); return context.readList(cls, encodedResponseBody); } - + @Override public Stream stream(Class cls) { final HttpResponse> res = handler(HttpResponse.BodyHandlers.ofLines()); @@ -445,6 +446,31 @@ public Stream stream(Class cls) { return res.body().map(bodyReader::readBody); } + + @Override + public T bean(ParameterizedType cls) { + readResponseContent(); + return context.readBean(cls, encodedResponseBody); + } + + @Override + public List list(ParameterizedType cls) { + readResponseContent(); + return context.readList(cls, encodedResponseBody); + } + + + @Override + public Stream stream(ParameterizedType cls) { + final HttpResponse> res = handler(HttpResponse.BodyHandlers.ofLines()); + this.httpResponse = res; + if (res.statusCode() >= 300) { + throw new HttpException(res, context); + } + final BodyReader bodyReader = context.beanReader(cls); + return res.body().map(bodyReader::readBody); + } + @Override public HttpResponse handler(HttpResponse.BodyHandler responseHandler) { final HttpResponse response = sendWith(responseHandler); diff --git a/client/src/main/java/io/avaje/http/client/HttpClientResponse.java b/client/src/main/java/io/avaje/http/client/HttpClientResponse.java index 10c2d82..13f4e65 100644 --- a/client/src/main/java/io/avaje/http/client/HttpClientResponse.java +++ b/client/src/main/java/io/avaje/http/client/HttpClientResponse.java @@ -1,6 +1,7 @@ package io.avaje.http.client; import java.io.InputStream; +import java.lang.reflect.ParameterizedType; import java.net.http.HttpResponse; import java.nio.file.Path; import java.util.List; @@ -85,6 +86,7 @@ public interface HttpClientResponse { */ List list(Class type); + /** * Return the response as a stream of beans. *

@@ -106,6 +108,51 @@ public interface HttpClientResponse { */ Stream stream(Class type); + /** + * Return the response as a single bean. + *

+ * If the HTTP statusCode is not in the 2XX range a HttpException is throw which contains + * the HttpResponse. This is the cause in the CompletionException. + * + * @param type The parameterized type of the bean to convert the response content into. + * @return The bean the response is converted into. + * @throws HttpException when the response has error status codes + */ + T bean(ParameterizedType type); + + /** + * Return the response as a list of beans. + *

+ * If the HTTP statusCode is not in the 2XX range a HttpException is throw which contains + * the HttpResponse. This is the cause in the CompletionException. + * + * @param type The parameterized type of the bean to convert the response content into. + * @return The list of beans the response is converted into. + * @throws HttpException when the response has error status codes + */ + List list(ParameterizedType type); + + /** + * Return the response as a stream of beans. + *

+ * Typically the response is expected to be {@literal application/x-json-stream} + * newline delimited json payload. + *

+ * Note that for this stream request the response content is not deemed + * 'loggable' by avaje-http-client. This is because the entire response + * may not be available at the time of the callback. As such {@link RequestLogger} + * will not include response content when logging stream request/response + *

+ * If the HTTP statusCode is not in the 2XX range a HttpException is throw which contains + * the HttpResponse. This is the cause in the CompletionException. + * + * @param type The parameterized type of the bean to convert the response content into. + * @return The stream of beans from the response + * @throws HttpException when the response has error status codes + */ + Stream stream(ParameterizedType type); + + /** * Return the response with check for 200 range status code. *

diff --git a/client/src/main/java/io/avaje/http/client/JsonbBodyAdapter.java b/client/src/main/java/io/avaje/http/client/JsonbBodyAdapter.java index b678072..610c877 100644 --- a/client/src/main/java/io/avaje/http/client/JsonbBodyAdapter.java +++ b/client/src/main/java/io/avaje/http/client/JsonbBodyAdapter.java @@ -1,13 +1,15 @@ package io.avaje.http.client; -import io.avaje.jsonb.JsonType; -import io.avaje.jsonb.Jsonb; - +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import io.avaje.jsonb.JsonType; +import io.avaje.jsonb.Jsonb; + /** - * avaje jsonb BodyAdapter to read and write beans as JSON. + * Avaje Jsonb BodyAdapter to read and write beans as JSON. * *

{@code
  *
@@ -21,9 +23,9 @@
 public final class JsonbBodyAdapter implements BodyAdapter {
 
   private final Jsonb jsonb;
-  private final ConcurrentHashMap, BodyWriter> beanWriterCache = new ConcurrentHashMap<>();
-  private final ConcurrentHashMap, BodyReader> beanReaderCache = new ConcurrentHashMap<>();
-  private final ConcurrentHashMap, BodyReader> listReaderCache = new ConcurrentHashMap<>();
+  private final ConcurrentHashMap> beanWriterCache = new ConcurrentHashMap<>();
+  private final ConcurrentHashMap> beanReaderCache = new ConcurrentHashMap<>();
+  private final ConcurrentHashMap> listReaderCache = new ConcurrentHashMap<>();
 
   /**
    * Create passing the Jsonb to use.
@@ -36,7 +38,7 @@ public JsonbBodyAdapter(Jsonb jsonb) {
    * Create with a default Jsonb that allows unknown properties.
    */
   public JsonbBodyAdapter() {
-    this.jsonb = Jsonb.newBuilder().build();
+    this.jsonb = Jsonb.builder().build();
   }
 
   @SuppressWarnings("unchecked")
@@ -51,6 +53,18 @@ public  BodyReader beanReader(Class cls) {
     return (BodyReader) beanReaderCache.computeIfAbsent(cls, aClass -> new JReader<>(jsonb.type(cls)));
   }
 
+  @SuppressWarnings("unchecked")
+  @Override
+  public  BodyReader beanReader(ParameterizedType cls) {
+    return (BodyReader) beanReaderCache.computeIfAbsent(cls, aClass -> new JReader<>(jsonb.type(cls)));
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public  BodyReader> listReader(ParameterizedType cls) {
+    return (BodyReader>) listReaderCache.computeIfAbsent(cls, aClass -> new JReader<>(jsonb.type(cls).list()));
+  }
+
   @SuppressWarnings("unchecked")
   @Override
   public  BodyReader> listReader(Class cls) {
diff --git a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java
index 516b118..6b308ad 100644
--- a/client/src/test/java/io/avaje/http/client/HelloControllerTest.java
+++ b/client/src/test/java/io/avaje/http/client/HelloControllerTest.java
@@ -394,7 +394,7 @@ void get_notFound() {
     final HttpResponse hres = request.GET().asString();
 
     assertThat(hres.statusCode()).isEqualTo(404);
-    assertThat(hres.body()).contains("Not found");
+    assertThat(hres.body()).contains("Not Found");
     HttpClientContext.Metrics metrics = clientContext.metrics(true);
     assertThat(metrics.totalCount()).isEqualTo(1);
     assertThat(metrics.errorCount()).isEqualTo(1);

From 27f9c81a618707726483a5cb319557e2623d8749 Mon Sep 17 00:00:00 2001
From: Josiah Noel <32279667+SentryMan@users.noreply.github.com>
Date: Wed, 28 Dec 2022 19:53:09 -0500
Subject: [PATCH 2/5] Update BodyAdapter.java

---
 client/src/main/java/io/avaje/http/client/BodyAdapter.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/src/main/java/io/avaje/http/client/BodyAdapter.java b/client/src/main/java/io/avaje/http/client/BodyAdapter.java
index 012e503..33e82f8 100644
--- a/client/src/main/java/io/avaje/http/client/BodyAdapter.java
+++ b/client/src/main/java/io/avaje/http/client/BodyAdapter.java
@@ -47,6 +47,6 @@ default  BodyReader beanReader(ParameterizedType type) {
    * @param type The bean type to convert the content to.
    */
   default  BodyReader> listReader(ParameterizedType type) {
-    throw new UnsupportedOperationException("Parameterized types not supported for this adapter");
+    throw new UnsupportedOperationException("Parameterized types not supported for this Body Adapter");
   }
 }

From eaf229559dbfd6c37920e9ec7be2eca39ff941d8 Mon Sep 17 00:00:00 2001
From: Josiah Noel <32279667+SentryMan@users.noreply.github.com>
Date: Wed, 28 Dec 2022 20:01:57 -0500
Subject: [PATCH 3/5] Revert "Update BodyAdapter.java"

This reverts commit 27f9c81a618707726483a5cb319557e2623d8749.
---
 client/src/main/java/io/avaje/http/client/BodyAdapter.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/src/main/java/io/avaje/http/client/BodyAdapter.java b/client/src/main/java/io/avaje/http/client/BodyAdapter.java
index 33e82f8..012e503 100644
--- a/client/src/main/java/io/avaje/http/client/BodyAdapter.java
+++ b/client/src/main/java/io/avaje/http/client/BodyAdapter.java
@@ -47,6 +47,6 @@ default  BodyReader beanReader(ParameterizedType type) {
    * @param type The bean type to convert the content to.
    */
   default  BodyReader> listReader(ParameterizedType type) {
-    throw new UnsupportedOperationException("Parameterized types not supported for this Body Adapter");
+    throw new UnsupportedOperationException("Parameterized types not supported for this adapter");
   }
 }

From 7c283af8195801392148c86835f2fc8b5c8b71e0 Mon Sep 17 00:00:00 2001
From: Josiah Noel <32279667+SentryMan@users.noreply.github.com>
Date: Wed, 28 Dec 2022 21:52:15 -0500
Subject: [PATCH 4/5] heh, forgot about the async and httpcall

---
 .../java/io/avaje/http/client/DHttpAsync.java | 22 ++++++
 .../java/io/avaje/http/client/DHttpCall.java  | 67 +++++++++++++++++--
 .../avaje/http/client/DHttpClientRequest.java | 22 ++++++
 .../avaje/http/client/HttpAsyncResponse.java  | 26 +++++++
 .../avaje/http/client/HttpCallResponse.java   | 25 +++++++
 5 files changed, 156 insertions(+), 6 deletions(-)

diff --git a/client/src/main/java/io/avaje/http/client/DHttpAsync.java b/client/src/main/java/io/avaje/http/client/DHttpAsync.java
index 3ab02cb..770f6d3 100644
--- a/client/src/main/java/io/avaje/http/client/DHttpAsync.java
+++ b/client/src/main/java/io/avaje/http/client/DHttpAsync.java
@@ -1,6 +1,7 @@
 package io.avaje.http.client;
 
 import java.io.InputStream;
+import java.lang.reflect.ParameterizedType;
 import java.net.http.HttpResponse;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
@@ -78,4 +79,25 @@ public  CompletableFuture> stream(Class type) {
       .performSendAsync(false, HttpResponse.BodyHandlers.ofLines())
       .thenApply(httpResponse -> request.asyncStream(type, httpResponse));
   }
+
+  @Override
+  public  CompletableFuture bean(ParameterizedType type) {
+    return request
+      .performSendAsync(true, HttpResponse.BodyHandlers.ofByteArray())
+      .thenApply(httpResponse -> request.asyncBean(type, httpResponse));
+  }
+
+  @Override
+  public  CompletableFuture> list(ParameterizedType type) {
+    return request
+      .performSendAsync(true, HttpResponse.BodyHandlers.ofByteArray())
+      .thenApply(httpResponse -> request.asyncList(type, httpResponse));
+  }
+
+  @Override
+  public  CompletableFuture> stream(ParameterizedType type) {
+    return request
+      .performSendAsync(false, HttpResponse.BodyHandlers.ofLines())
+      .thenApply(httpResponse -> request.asyncStream(type, httpResponse));
+  }
 }
diff --git a/client/src/main/java/io/avaje/http/client/DHttpCall.java b/client/src/main/java/io/avaje/http/client/DHttpCall.java
index 3ee7a8e..31c94e9 100644
--- a/client/src/main/java/io/avaje/http/client/DHttpCall.java
+++ b/client/src/main/java/io/avaje/http/client/DHttpCall.java
@@ -1,6 +1,7 @@
 package io.avaje.http.client;
 
 import java.io.InputStream;
+import java.lang.reflect.ParameterizedType;
 import java.net.http.HttpResponse;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
@@ -64,6 +65,21 @@ public  HttpCall> stream(Class type) {
     return new CallStream<>(type);
   }
 
+  @Override
+  public  HttpCall bean(ParameterizedType type) {
+    return new CallBean<>(type);
+  }
+
+  @Override
+  public  HttpCall> list(ParameterizedType type) {
+    return new CallList<>(type);
+  }
+
+  @Override
+  public  HttpCall> stream(ParameterizedType type) {
+    return new CallStream<>(type);
+  }
+
   private class CallVoid implements HttpCall> {
     @Override
     public HttpResponse execute() {
@@ -132,46 +148,85 @@ public CompletableFuture> async() {
 
   private class CallBean implements HttpCall {
     private final Class type;
+    private final ParameterizedType genericType;
+    private final boolean isGeneric;
+
     CallBean(Class type) {
+      this.isGeneric = false;
       this.type = type;
+      this.genericType = null;
     }
+
+    CallBean(ParameterizedType type) {
+      this.isGeneric = true;
+      this.type = null;
+      this.genericType = type;
+    }
+
     @Override
     public E execute() {
-      return request.bean(type);
+      return isGeneric ? request.bean(genericType) : request.bean(type);
     }
+
     @Override
     public CompletableFuture async() {
-      return request.async().bean(type);
+      return isGeneric ? request.async().bean(genericType) : request.async().bean(type);
     }
   }
 
   private class CallList implements HttpCall> {
     private final Class type;
+    private final ParameterizedType genericType;
+    private final boolean isGeneric;
+
     CallList(Class type) {
+      this.isGeneric = false;
       this.type = type;
+      this.genericType = null;
     }
+
+    CallList(ParameterizedType type) {
+      this.isGeneric = true;
+      this.type = null;
+      this.genericType = type;
+    }
+
     @Override
     public List execute() {
-      return request.list(type);
+      return isGeneric ? request.list(genericType) : request.list(type);
     }
+
     @Override
     public CompletableFuture> async() {
-      return request.async().list(type);
+      return isGeneric ? request.async().list(genericType) : request.async().list(type);
     }
   }
 
   private class CallStream implements HttpCall> {
     private final Class type;
+    private final ParameterizedType genericType;
+    private final boolean isGeneric;
+
     CallStream(Class type) {
+      this.isGeneric = false;
       this.type = type;
+      this.genericType = null;
+    }
+
+    CallStream(ParameterizedType type) {
+      this.isGeneric = true;
+      this.type = null;
+      this.genericType = type;
     }
+
     @Override
     public Stream execute() {
-      return request.stream(type);
+      return isGeneric ? request.stream(genericType) : request.stream(type);
     }
+
     @Override
     public CompletableFuture> async() {
-      return request.async().stream(type);
+      return isGeneric ? request.async().stream(genericType) : request.async().stream(type);
     }
   }
 
diff --git a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java
index 5fc3e6c..22d885f 100644
--- a/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java
+++ b/client/src/main/java/io/avaje/http/client/DHttpClientRequest.java
@@ -532,6 +532,28 @@ protected  Stream asyncStream(Class type, HttpResponse>
     return response.body().map(bodyReader::readBody);
   }
 
+  protected  E asyncBean(ParameterizedType type, HttpResponse response) {
+    afterAsyncEncoded(response);
+    return context.readBean(type, encodedResponseBody);
+  }
+
+  protected  List asyncList(ParameterizedType type, HttpResponse response) {
+    afterAsyncEncoded(response);
+    return context.readList(type, encodedResponseBody);
+  }
+
+  protected  Stream asyncStream(
+      ParameterizedType type, HttpResponse> response) {
+    responseTimeNanos = System.nanoTime() - startAsyncNanos;
+    httpResponse = response;
+    context.afterResponse(this);
+    if (response.statusCode() >= 300) {
+      throw new HttpException(response, context);
+    }
+    final BodyReader bodyReader = context.beanReader(type);
+    return response.body().map(bodyReader::readBody);
+  }
+
   private void afterAsyncEncoded(HttpResponse response) {
     responseTimeNanos = System.nanoTime() - startAsyncNanos;
     httpResponse = response;
diff --git a/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java b/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java
index 45cea96..7e6dee8 100644
--- a/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java
+++ b/client/src/main/java/io/avaje/http/client/HttpAsyncResponse.java
@@ -1,6 +1,7 @@
 package io.avaje.http.client;
 
 import java.io.InputStream;
+import java.lang.reflect.ParameterizedType;
 import java.net.http.HttpResponse;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
@@ -334,4 +335,29 @@ default  CompletableFuture> withHandler(HttpResponse.BodyHand
    * @return The CompletableFuture of the response
    */
    CompletableFuture> stream(Class type);
+
+  /**
+   * Process expecting a bean response body (typically from json content).
+   *
+   * @param type The parameterized type to convert the content to
+   * @return The CompletableFuture of the response
+   */
+   CompletableFuture bean(ParameterizedType type);
+
+  /**
+   * Process expecting a list of beans response body (typically from json content).
+   *
+   * @param type The parameterized type to convert the content to
+   * @return The CompletableFuture of the response
+   */
+   CompletableFuture> list(ParameterizedType type);
+
+  /**
+   * Process response as a stream of beans (x-json-stream).
+   *
+   * @param type The parameterized type to convert the content to
+   * @return The CompletableFuture of the response
+   */
+   CompletableFuture> stream(ParameterizedType type);
+
 }
diff --git a/client/src/main/java/io/avaje/http/client/HttpCallResponse.java b/client/src/main/java/io/avaje/http/client/HttpCallResponse.java
index d081a32..96cccbe 100644
--- a/client/src/main/java/io/avaje/http/client/HttpCallResponse.java
+++ b/client/src/main/java/io/avaje/http/client/HttpCallResponse.java
@@ -1,6 +1,7 @@
 package io.avaje.http.client;
 
 import java.io.InputStream;
+import java.lang.reflect.ParameterizedType;
 import java.net.http.HttpResponse;
 import java.util.List;
 import java.util.stream.Stream;
@@ -186,4 +187,28 @@ default  HttpCall> withHandler(HttpResponse.BodyHandler bo
    */
    HttpCall> stream(Class type);
 
+  /**
+   * A bean response to execute async or sync.
+   *
+   * @param type The parameterized type to convert the content to
+   * @return The HttpCall to allow sync or async execution
+   */
+   HttpCall bean(ParameterizedType type);
+
+  /**
+   * Process expecting a list of beans response body (typically from json content).
+   *
+   * @param type The parameterized type to convert the content to
+   * @return The HttpCall to execute sync or async
+   */
+   HttpCall> list(ParameterizedType type);
+
+  /**
+   * Process expecting a stream of beans response body (typically from json content).
+   *
+   * @param type The parameterized type to convert the content to
+   * @return The HttpCall to execute sync or async
+   */
+   HttpCall> stream(ParameterizedType type);
+
 }

From 61619e55eb14de11611b8358b8bb2ac00bdf57e0 Mon Sep 17 00:00:00 2001
From: Josiah Noel <32279667+SentryMan@users.noreply.github.com>
Date: Sat, 31 Dec 2022 13:42:43 -0600
Subject: [PATCH 5/5] Update README.md

---
 README.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/README.md b/README.md
index f87cec7..5513b53 100644
--- a/README.md
+++ b/README.md
@@ -310,7 +310,7 @@ public final class ExampleRetry implements RetryHandler {
 
     final var code = response.statusCode();
 
-    if (retryCount >= MAX_RETRIES || code >= 400) {
+    if (retryCount >= MAX_RETRIES || code <= 400) {
 
       return false;
     }