55 */
66package com .gooddata .sdk .common ;
77
8+
89import org .apache .http .HttpEntityEnclosingRequest ;
910import org .apache .http .HttpRequest ;
1011import org .apache .http .client .HttpClient ;
1112import org .apache .http .client .methods .*;
1213import org .apache .http .entity .ByteArrayEntity ;
13- import org .apache .http .protocol .HttpContext ;
14+ import org .slf4j .Logger ;
15+ import org .slf4j .LoggerFactory ;
1416
1517import org .springframework .http .HttpHeaders ;
1618import org .springframework .http .HttpMethod ;
2325import java .io .IOException ;
2426import java .io .OutputStream ;
2527import java .net .URI ;
28+ // import java.nio.charset.StandardCharsets; // Commented out as not needed when debug logging is disabled
29+ import java .util .List ;
30+ import java .util .Map ;
2631
2732/**
2833 * Spring 6 compatible {@link ClientHttpRequestFactory} implementation that uses Apache HttpComponents HttpClient 4.x.
3136 */
3237public class HttpClient4ComponentsClientHttpRequestFactory implements ClientHttpRequestFactory {
3338
39+ private static final Logger logger = LoggerFactory .getLogger (HttpClient4ComponentsClientHttpRequestFactory .class );
3440 private final HttpClient httpClient ;
3541
3642 /**
@@ -121,19 +127,77 @@ public OutputStream getBody() throws IOException {
121127
122128 @ Override
123129 public ClientHttpResponse execute () throws IOException {
124- addHeaders (httpRequest );
125-
126- if (httpRequest instanceof HttpEntityEnclosingRequest ) {
127- HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest ) httpRequest ;
128- if (bufferedOutput .size () > 0 ) {
129- ByteArrayEntity entity = new ByteArrayEntity (bufferedOutput .toByteArray ());
130- // Don't set Content-Type or Content-Encoding on entity to avoid conflicts
131- // Spring's message converters have already written properly formatted content
132- // and set the appropriate headers
133- entityRequest .setEntity (entity );
130+ // Create entity first (matching reference implementation exactly)
131+ byte [] bytes = bufferedOutput .toByteArray ();
132+ if (bytes .length > 0 ) {
133+ if (httpRequest instanceof HttpEntityEnclosingRequest ) {
134+ HttpEntityEnclosingRequest entityRequest = (HttpEntityEnclosingRequest ) httpRequest ;
135+
136+ // Ensure proper UTF-8 encoding before creating entity
137+ // This is crucial for @JsonTypeInfo annotated classes like Execution
138+ ByteArrayEntity requestEntity = new ByteArrayEntity (bytes );
139+
140+ // ВАЖНО: НЕ устанавливаем Content-Type на entity!
141+ // Это предотвращает дублирование заголовков и конфликты с GoodData API
142+ // Content-Type будет управляться только через HTTP заголовки в addHeaders()
143+ if (logger .isDebugEnabled ()) {
144+ // Проверяем какой Content-Type мы получим от заголовков
145+ boolean hasContentType = false ;
146+ for (org .apache .http .Header header : httpRequest .getAllHeaders ()) {
147+ if ("Content-Type" .equalsIgnoreCase (header .getName ())) {
148+ hasContentType = true ;
149+ // String contentType = header.getValue();
150+ // logger.debug("Content-Type from headers: {}", contentType);
151+ break ;
152+ }
153+ }
154+
155+ if (!hasContentType ) {
156+ // logger.debug("Default Content-Type set: application/json; charset=UTF-8");
157+ }
158+ }
159+
160+ entityRequest .setEntity (requestEntity );
161+
162+ // DEBUG: Enhanced logging for request debugging including @JsonTypeInfo issues
163+ // if (bytes.length > 0 && logger.isDebugEnabled()) {
164+ // logger.debug("Request Body Length: {} bytes", bytes.length);
165+ //
166+ // // Get final Content-Type from entity for logging
167+ // org.apache.http.Header finalContentType = entityRequest.getEntity().getContentType();
168+ // String contentType = finalContentType != null ? finalContentType.getValue() : "none";
169+ // logger.debug("Final entity Content-Type: {}", contentType);
170+
171+ // // Only log text-based requests to avoid binary data issues
172+ // if (contentType.contains("application/json") || contentType.contains("text/")) {
173+ // try {
174+ // // Use charset from Content-Type if available, otherwise UTF-8
175+ // java.nio.charset.Charset charset = StandardCharsets.UTF_8;
176+ // if (contentType.contains("charset=")) {
177+ // String charsetName = contentType.substring(contentType.indexOf("charset=") + 8);
178+ // charsetName = charsetName.split(";")[0].trim();
179+ // charset = java.nio.charset.Charset.forName(charsetName);
180+ // }
181+ //
182+ // String requestBody = new String(bytes, charset);
183+ // // Log @JsonTypeInfo requests that might cause "malformed syntax" errors
184+ // if (requestBody.contains("\"execution\"") || requestBody.contains("\"report_req\"")) {
185+ // logger.debug("@JsonTypeInfo request ({}): {}", contentType, requestBody);
186+ // }
187+ // } catch (Exception e) {
188+ // logger.debug("Could not decode request body for logging: {}", e.getMessage());
189+ // }
190+ // } else if (contentType.contains("multipart/form-data")) {
191+ // logger.debug("Multipart form data request with Content-Type: {}", contentType);
192+ // }
193+ // }
134194 }
135195 }
136196
197+ // Set headers exactly like reference implementation
198+ // (no additional headers parameter in our case, but same logic)
199+ addHeaders (httpRequest );
200+
137201 // Handle both GoodDataHttpClient and standard HttpClient
138202 org .apache .http .HttpResponse httpResponse ;
139203 if (httpClient .getClass ().getName ().contains ("GoodDataHttpClient" )) {
@@ -163,24 +227,108 @@ public ClientHttpResponse execute() throws IOException {
163227
164228 /**
165229 * Add the headers from the HttpHeaders to the HttpRequest.
166- * Excludes Content-Length and Transfer-Encoding headers to avoid conflicts
167- * with HttpClient 4.x internal header management.
168- * Content-Type and Content-Encoding are included as HTTP headers for proper JSON support.
169- * Follows Spring 6 HttpComponentsClientHttpRequest.addHeaders implementation.
230+ * Excludes Content-Length headers to avoid conflicts with HttpClient 4.x internal management.
231+ * Uses setHeader instead of addHeader to match the reference implementation.
232+ * Follows HttpClient4ClientHttpRequest.executeInternal implementation pattern.
170233 */
171234 private void addHeaders (HttpRequest httpRequest ) {
172- headers .forEach ((headerName , headerValues ) -> {
173- if ("Cookie" .equalsIgnoreCase (headerName )) { // RFC 6265
174- String headerValue = String .join ("; " , headerValues );
175- httpRequest .addHeader (headerName , headerValue );
235+ // КРИТИЧНО для GoodData API: устанавливаем заголовки в фиксированном порядке
236+ // для стабильной чексуммы. Порядок: Accept, X-GDC-Version, Content-Type, остальные
237+
238+ // Сначала очищаем потенциально проблемные заголовки
239+ if (httpRequest instanceof HttpUriRequest ) {
240+ HttpUriRequest uriRequest = (HttpUriRequest ) httpRequest ;
241+ uriRequest .removeHeaders ("Accept" );
242+ uriRequest .removeHeaders ("X-GDC-Version" );
243+ uriRequest .removeHeaders ("Content-Type" );
244+ }
245+
246+ // 1. Accept заголовок (первый для стабильности чексуммы)
247+ if (headers .containsKey ("Accept" )) {
248+ String acceptValue = String .join (", " , headers .get ("Accept" ));
249+ httpRequest .setHeader ("Accept" , acceptValue );
250+ // if (logger.isDebugEnabled()) {
251+ // logger.debug("Header: Accept = {}", acceptValue);
252+ // }
253+ }
254+
255+ // 2. X-GDC-Version заголовок (второй для стабильности)
256+ if (headers .containsKey ("X-GDC-Version" )) {
257+ String versionValue = String .join (", " , headers .get ("X-GDC-Version" ));
258+ httpRequest .setHeader ("X-GDC-Version" , versionValue );
259+ // if (logger.isDebugEnabled()) {
260+ // logger.debug("Header: X-GDC-Version = {}", versionValue);
261+ // }
262+ }
263+
264+ // 3. Content-Type - управляется только через заголовки (без entity Content-Type)
265+ String finalContentType = null ;
266+ if (headers .containsKey ("Content-Type" )) {
267+ // Используем Spring заголовок Content-Type
268+ String contentTypeValue = String .join (", " , headers .get ("Content-Type" ));
269+ // Добавляем charset=UTF-8 для JSON если его нет
270+ if (contentTypeValue .contains ("application/json" ) && !contentTypeValue .contains ("charset=" )) {
271+ finalContentType = contentTypeValue + "; charset=UTF-8" ;
272+ // if (logger.isDebugEnabled()) {
273+ // logger.debug("Enhanced Content-Type for JSON: {}", finalContentType);
274+ // }
275+ } else {
276+ finalContentType = contentTypeValue ;
277+ // if (logger.isDebugEnabled()) {
278+ // logger.debug("Using Spring Content-Type: {}", finalContentType);
279+ // }
176280 }
177- else if (!"Content-Length" .equalsIgnoreCase (headerName ) &&
178- !"Transfer-Encoding" .equalsIgnoreCase (headerName )) {
179- for (String headerValue : headerValues ) {
180- httpRequest .addHeader (headerName , headerValue );
281+ } else if (httpRequest instanceof HttpEntityEnclosingRequest ) {
282+ // Устанавливаем дефолтный Content-Type для JSON запросов с телом
283+ finalContentType = "application/json; charset=UTF-8" ;
284+ // if (logger.isDebugEnabled()) {
285+ // logger.debug("Default Content-Type for JSON requests: {}", finalContentType);
286+ // }
287+ }
288+
289+ if (finalContentType != null ) {
290+ httpRequest .setHeader ("Content-Type" , finalContentType );
291+ // if (logger.isDebugEnabled()) {
292+ // logger.debug("Header: Content-Type = {}", finalContentType);
293+ // }
294+ }
295+
296+ // 4. Все остальные заголовки (в алфавитном порядке для стабильности)
297+ headers .entrySet ().stream ()
298+ .filter (entry -> {
299+ String headerName = entry .getKey ();
300+ return !"Content-Length" .equalsIgnoreCase (headerName ) &&
301+ !"Transfer-Encoding" .equalsIgnoreCase (headerName ) &&
302+ !"Content-Type" .equalsIgnoreCase (headerName ) &&
303+ !"Accept" .equalsIgnoreCase (headerName ) &&
304+ !"X-GDC-Version" .equalsIgnoreCase (headerName );
305+ })
306+ .sorted (Map .Entry .comparingByKey ()) // Алфавитный порядок для стабильности
307+ .forEach (entry -> {
308+ String headerName = entry .getKey ();
309+ List <String > headerValues = entry .getValue ();
310+
311+ String headerValue ;
312+ if ("Cookie" .equalsIgnoreCase (headerName )) { // RFC 6265
313+ headerValue = String .join ("; " , headerValues );
314+ } else {
315+ headerValue = String .join (", " , headerValues );
181316 }
182- }
183- });
317+
318+ httpRequest .setHeader (headerName , headerValue );
319+ // if (logger.isDebugEnabled()) {
320+ // logger.debug("Header: {} = {}", headerName, headerValue);
321+ // }
322+ });
323+
324+ // Log final headers state for checksum debugging
325+ // if (logger.isDebugEnabled()) {
326+ // org.apache.http.Header[] allHeaders = httpRequest.getAllHeaders();
327+ // logger.debug("Final request headers count: {}", allHeaders.length);
328+ // for (org.apache.http.Header header : allHeaders) {
329+ // logger.debug("Final header: {} = {}", header.getName(), header.getValue());
330+ // }
331+ // }
184332 }
185333 }
186334}
0 commit comments