An HTTP retry repeats a selected operation after a classified failure, so both replay safety and the total attempt budget belong to the client contract.
Java HTTP retry project: bounded attempts and one wait budget
Java 11+. The program uses JDK classes and requires no preview flags.
Retry a repeatable operation
The local server returns 503 on its first GET and 200 on its next GET. The client retries this read-only operation at most three times under one monotonic wait budget. A timeout or status does not automatically justify replaying a payment or another state-changing request.
Each attempt receives only the remaining budget. Starting a fresh full timeout for every attempt would let a request exceed its intended total wait. The program checks for exhaustion before sending and preserves interruption instead of treating it as another transient server failure.
This model retries a classified status only. It does not add random backoff, honor Retry-After or coordinate a shared rate limit. Those production policies need a representative load and service contract; a local two-response fixture cannot select them for every API.
Observe status before parsing
A received 503 is an HTTP response, not necessarily a transport exception. The client checks the status before treating the body as a quote. A response body parser also needs its own size and schema limits; the tiny synthetic body here is not an untrusted-data policy.
Working program
import com.sun.net.httpserver.HttpServer;
import java.net.*;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class RetriedLocalQuote {
static String fetch(HttpClient client,URI uri,Duration budget)throws Exception{
long deadline=System.nanoTime()+budget.toNanos();
for(int attempt=0;attempt<3;attempt++){
long remaining=deadline-System.nanoTime();if(remaining<=0)throw new TimeoutException("Quote budget");
HttpRequest request=HttpRequest.newBuilder(uri).timeout(Duration.ofNanos(remaining)).GET().build();
HttpResponse<String> response=client.send(request,HttpResponse.BodyHandlers.ofString());
if(response.statusCode()==200)return response.body();if(response.statusCode()!=503)throw new IllegalStateException("Unexpected HTTP status");
}throw new IllegalStateException("Attempt limit");
}
public static void main(String[] args)throws Exception{
AtomicInteger calls=new AtomicInteger();HttpServer server=HttpServer.create(new InetSocketAddress(InetAddress.getLoopbackAddress(),0),0);ExecutorService handlers=Executors.newSingleThreadExecutor();server.setExecutor(handlers);
server.createContext("/quote",exchange->{int attempt=calls.incrementAndGet();byte[] body=(attempt==1?"busy":"quote=150").getBytes(StandardCharsets.UTF_8);exchange.sendResponseHeaders(attempt==1?503:200,body.length);try(java.io.OutputStream output=exchange.getResponseBody()){output.write(body);}});server.start();
try{
URI uri=new URI("http",null,server.getAddress().getAddress().getHostAddress(),server.getAddress().getPort(),"/quote",null,null);
HttpClient client=HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(1)).build();System.out.println(fetch(client,uri,Duration.ofSeconds(2)));System.out.println("attempts="+calls.get());
}finally{server.stop(0);handlers.shutdownNow();if(!handlers.awaitTermination(2,TimeUnit.SECONDS))throw new IllegalStateException("Server worker");}
}
}Output
quote=150
attempts=2Costs and boundaries
At most three sequential attempts are admitted, each with a remaining request timeout. Cleanup has a separate bounded worker wait. This fixture does not establish an end-to-end deadline for arbitrary DNS, response processing or external side effects, and it is not a server load test.
Common Mistakes
- A failed response is not proof that a state-changing operation never took effect.
- Do not reset the entire budget for each retry.
- Interruption is not a server status to retry blindly.
Read next
Local response handling, Shared deadlines.
Apply this contract in Spring
Spring Boot receipt API project: build, test and inspect the limits. These lessons keep framework assembly separate from the Java contract.
