HttpFetchService.java
package net.sasasin.sreader.service.http;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;
import java.util.Objects;
import net.sasasin.sreader.service.outcome.FailureKind;
import net.sasasin.sreader.service.outcome.FailureStage;
import net.sasasin.sreader.service.outcome.OperationFailure;
import org.springframework.stereotype.Service;
/**
* Shared HTTP GET and redirect resolution for feed/article fetch paths that do not require
* per-chain cookie isolation. Uses the application-scoped {@link HttpClient} bean.
*/
@Service
public class HttpFetchService {
private final HttpTransport transport;
private final HttpClient client;
public HttpFetchService(HttpTransport transport, HttpClient client) {
this.transport = Objects.requireNonNull(transport, "transport must not be null");
this.client = Objects.requireNonNull(client, "client must not be null");
}
public FetchedResource get(URI uri) throws IOException, InterruptedException {
HttpTransport.FetchedBytes fetched = transport.get(client, uri);
return new FetchedResource(fetched.finalUri(), fetched.body());
}
public RedirectResolution resolveRedirect(URI uri) {
try {
HttpResponse<Void> response = transport.sendDiscarding(client, transport.headRequest(uri));
if (response.statusCode() >= 200 && response.statusCode() < 400) {
return new RedirectResolution.Resolved(uri, response.uri());
}
return new RedirectResolution.Fallback(
uri,
OperationFailure.of(
FailureStage.RESOLVE_REDIRECT,
FailureKind.HTTP_STATUS,
uri.toString(),
"HEAD " + uri + " returned HTTP " + response.statusCode()));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return new RedirectResolution.Fallback(
uri,
OperationFailure.of(
FailureStage.RESOLVE_REDIRECT,
FailureKind.INTERRUPTED,
uri.toString(),
"Redirect resolution interrupted for " + uri,
e));
} catch (IOException e) {
return new RedirectResolution.Fallback(
uri,
OperationFailure.of(
FailureStage.RESOLVE_REDIRECT,
FailureKind.IO,
uri.toString(),
"Redirect resolution I/O failure for " + uri + ": " + e.getMessage(),
e));
} catch (IllegalArgumentException e) {
return new RedirectResolution.Fallback(
uri,
OperationFailure.of(
FailureStage.RESOLVE_REDIRECT,
FailureKind.INVALID_INPUT,
uri.toString(),
"Redirect resolution invalid input for " + uri + ": " + e.getMessage(),
e));
}
}
public record FetchedResource(URI uri, String body) {}
}