1010import java .net .http .HttpResponse ;
1111import java .net .http .HttpResponse .BodyHandlers ;
1212import java .time .Duration ;
13+ import java .util .ArrayList ;
14+ import java .util .LinkedHashMap ;
15+ import java .util .List ;
16+ import java .util .Map ;
1317import java .util .Set ;
1418
1519import org .slf4j .Logger ;
@@ -26,66 +30,69 @@ public class ClientService {
2630 private static final Set <String > BAD_HEADERS = Set .of (
2731 "host" , "content-length" , "connection" , "accept-encoding" , "upgrade"
2832 );
33+ private static final Set <Integer > REDIRECT_CODES = Set .of (301 , 302 , 303 , 307 , 308 );
34+ private static final int MAX_REDIRECTS = 10 ;
35+
2936 private final HttpClient client ;
3037
3138 public ClientService () {
3239 client = HttpClient .newBuilder ()
3340 .version (Version .HTTP_2 )
34- .followRedirects (Redirect .NORMAL )
41+ .followRedirects (Redirect .NEVER )
3542 .connectTimeout (Duration .ofSeconds (10 ))
3643 .build ();
3744 }
3845
3946 public ResponseEntity <String > sendRequest (JavaHttpRequest requestData , HttpHeaders incomingHeaders ) {
4047 try {
41- boolean hasBody = requestData .body () != null && !requestData .body ().isBlank ();
42-
43- HttpRequest .BodyPublisher bodyPublisher = hasBody
44- ? HttpRequest .BodyPublishers .ofString (requestData .body ())
45- : HttpRequest .BodyPublishers .noBody ();
46-
47- HttpRequest .Builder builder = HttpRequest .newBuilder ()
48- .uri (URI .create (requestData .url ()))
49- .timeout (Duration .ofSeconds (30 ))
50- .method (requestData .method ().name (), bodyPublisher );
51-
52- if (hasBody && requestData .contentType () != null && !requestData .contentType ().isBlank ()) {
53- builder .header ("Content-Type" , requestData .contentType ());
54- } else if (hasBody ) {
55- builder .header ("Content-Type" , "application/json" );
56- }
57-
58- if (requestData .copyHeaders () && incomingHeaders != null ) {
59- incomingHeaders .forEach ((key , value ) -> {
60- if (!BAD_HEADERS .contains (key .toLowerCase ()) && !value .isEmpty ()) {
61- try {
62- builder .header (key , value .get (0 ));
63- } catch (IllegalArgumentException e ) {
64- logger .warn ("Header {} ist geschützt und wurde übersprungen" , key );
65- }
66- }
67- });
48+ List <Map <String , Object >> redirectChain = new ArrayList <>();
49+ URI currentUri = URI .create (requestData .url ());
50+ String currentMethod = requestData .method ().name ();
51+ String currentBody = requestData .body ();
52+
53+ // Erster Request mit allen Headern
54+ HttpResponse <String > response = client .send (
55+ buildRequest (currentUri , currentMethod , currentBody , requestData , incomingHeaders ),
56+ BodyHandlers .ofString ());
57+
58+ // Redirects manuell verfolgen
59+ while (REDIRECT_CODES .contains (response .statusCode ()) && redirectChain .size () < MAX_REDIRECTS ) {
60+ String location = response .headers ().firstValue ("location" ).orElse (null );
61+ if (location == null ) break ;
62+
63+ URI nextUri = currentUri .resolve (location );
64+
65+ Map <String , Object > step = new LinkedHashMap <>();
66+ step .put ("from" , currentUri .toString ());
67+ step .put ("status" , response .statusCode ());
68+ step .put ("to" , nextUri .toString ());
69+ step .put ("proto" , response .version () == Version .HTTP_2 ? "HTTP/2" : "HTTP/1.1" );
70+ redirectChain .add (step );
71+
72+ currentUri = nextUri ;
73+ // 307/308: Methode + Body beibehalten; alle anderen → GET ohne Body
74+ if (response .statusCode () != 307 && response .statusCode () != 308 ) {
75+ currentMethod = "GET" ;
76+ currentBody = null ;
77+ }
78+
79+ // Folge-Requests ohne originale Browser-Header (kein Auth-Leak)
80+ response = client .send (
81+ buildRequest (currentUri , currentMethod , currentBody , null , null ),
82+ BodyHandlers .ofString ());
6883 }
6984
70- if (requestData .customHeaders () != null ) {
71- requestData .customHeaders ().forEach ((key , value ) -> {
72- if (key != null && !key .isBlank () && !BAD_HEADERS .contains (key .toLowerCase ())) {
73- builder .header (key , value );
74- }
75- });
76- }
77-
78- HttpResponse <String > response = client .send (builder .build (), BodyHandlers .ofString ());
79-
8085 HttpHeaders responseHeaders = new HttpHeaders ();
8186 response .headers ().map ().forEach (responseHeaders ::addAll );
82-
8387 String protocolVersion = response .version () == Version .HTTP_2 ? "HTTP/2" : "HTTP/1.1" ;
8488
8589 return ResponseEntity .status (response .statusCode ())
8690 .headers (h -> {
8791 h .addAll (responseHeaders );
8892 h .set ("X-Protocol-Version" , protocolVersion );
93+ if (!redirectChain .isEmpty ()) {
94+ h .set ("X-Redirect-Chain" , serializeChain (redirectChain ));
95+ }
8996 })
9097 .body (response .body ());
9198
@@ -103,11 +110,72 @@ public ResponseEntity<String> sendRequest(JavaHttpRequest requestData, HttpHeade
103110 }
104111 }
105112
113+ private HttpRequest buildRequest (URI uri , String method , String body ,
114+ JavaHttpRequest requestData , HttpHeaders incomingHeaders ) {
115+
116+ boolean hasBody = body != null && !body .isBlank ()
117+ && !method .equals ("GET" ) && !method .equals ("HEAD" ) && !method .equals ("OPTIONS" );
118+
119+ HttpRequest .BodyPublisher bodyPublisher = hasBody
120+ ? HttpRequest .BodyPublishers .ofString (body )
121+ : HttpRequest .BodyPublishers .noBody ();
122+
123+ HttpRequest .Builder builder = HttpRequest .newBuilder ()
124+ .uri (uri )
125+ .timeout (Duration .ofSeconds (30 ))
126+ .method (method , bodyPublisher );
127+
128+ if (hasBody && requestData != null ) {
129+ String ct = requestData .contentType ();
130+ builder .header ("Content-Type" , ct != null && !ct .isBlank () ? ct : "application/json" );
131+ }
132+
133+ if (requestData != null && requestData .copyHeaders () && incomingHeaders != null ) {
134+ incomingHeaders .forEach ((key , value ) -> {
135+ if (!BAD_HEADERS .contains (key .toLowerCase ()) && !value .isEmpty ()) {
136+ try {
137+ builder .header (key , value .get (0 ));
138+ } catch (IllegalArgumentException e ) {
139+ logger .warn ("Header {} ist geschützt und wurde übersprungen" , key );
140+ }
141+ }
142+ });
143+ }
144+
145+ if (requestData != null && requestData .customHeaders () != null ) {
146+ requestData .customHeaders ().forEach ((key , value ) -> {
147+ if (key != null && !key .isBlank () && !BAD_HEADERS .contains (key .toLowerCase ())) {
148+ builder .header (key , value );
149+ }
150+ });
151+ }
152+
153+ return builder .build ();
154+ }
155+
156+ private String serializeChain (List <Map <String , Object >> chain ) {
157+ StringBuilder sb = new StringBuilder ("[" );
158+ for (int i = 0 ; i < chain .size (); i ++) {
159+ if (i > 0 ) sb .append ("," );
160+ Map <String , Object > step = chain .get (i );
161+ sb .append ("{" )
162+ .append ("\" from\" :\" " ).append (escapeJson (step .get ("from" ).toString ())).append ("\" ," )
163+ .append ("\" status\" :" ).append (step .get ("status" )).append ("," )
164+ .append ("\" to\" :\" " ).append (escapeJson (step .get ("to" ).toString ())).append ("\" ," )
165+ .append ("\" proto\" :\" " ).append (step .get ("proto" )).append ("\" " )
166+ .append ("}" );
167+ }
168+ return sb .append ("]" ).toString ();
169+ }
170+
171+ private String escapeJson (String s ) {
172+ return s .replace ("\\ " , "\\ \\ " ).replace ("\" " , "\\ \" " );
173+ }
174+
106175 private String formatErrorResponse (Exception e ) {
107176 String summary = "Unbekannter Fehler" ;
108177 String detail = e .getMessage () != null ? e .getMessage () : "Keine Nachricht" ;
109178
110- // Spezifische K8s/Netzwerk-Szenarien
111179 if (e instanceof java .net .UnknownHostException ) {
112180 summary = "DNS Fehler: Host nicht gefunden. (Service-Name korrekt? Namespace vergessen?)" ;
113181 } else if (e instanceof java .net .ConnectException ) {
@@ -123,4 +191,4 @@ private String formatErrorResponse(Exception e) {
123191
124192 return String .format ("%s\n Details: %s\n ---STACKTRACE---\n %s" , summary , detail , sw .toString ());
125193 }
126- }
194+ }
0 commit comments