Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 114 additions & 6 deletions src/main/java/com/adyen/httpclient/AdyenHttpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
import org.apache.hc.core5.http.io.entity.StringEntity;
import org.apache.hc.core5.net.URIBuilder;
import org.apache.hc.core5.ssl.SSLContexts;
Expand Down Expand Up @@ -212,6 +213,30 @@
return response.getBody();
}

@Override
public String requestBinary(
String endpoint,
BinaryRequestBody requestBody,
Config config,
boolean isApiKeyRequired,
RequestOptions requestOptions,
ApiConstants.HttpMethod httpMethod,
Map<String, String> params)
throws IOException, HTTPClientException {
CloseableHttpClient httpclient = getOrCreateHttpClient(config);
HttpUriRequestBase httpRequest =
createRequest(
endpoint, requestBody, config, isApiKeyRequired, requestOptions, httpMethod, params);

AdyenResponse response = httpclient.execute(httpRequest, new AdyenResponseHandler());

if (response.getStatus() < 200 || response.getStatus() >= 300) {
throw new HTTPClientException(
response.getStatus(), "HTTP Exception", response.getHeaders(), response.getBody());
}
return response.getBody();
}

/**
* Builds an {@link HttpUriRequestBase} with the appropriate HTTP method, headers, authentication,
* and per-request timeout configuration from {@link Config}.
Expand Down Expand Up @@ -256,15 +281,56 @@
httpRequest.setConfig(builder.build());

setAuthentication(httpRequest, isApiKeyRequired, config);
setHeaders(config, requestOptions, httpRequest);
setHeaders(config, requestOptions, httpRequest, null);

return httpRequest;
}

private void setHeaders(
Config config, RequestOptions requestOptions, HttpUriRequestBase httpUriRequest) {
HttpUriRequestBase createRequest(
String endpoint,
BinaryRequestBody requestBody,
Config config,
boolean isApiKeyRequired,
RequestOptions requestOptions,
ApiConstants.HttpMethod httpMethod,
Map<String, String> params)
throws HTTPClientException {
HttpUriRequestBase httpRequest =
createHttpRequestBase(createUri(endpoint, params), requestBody.getData(), httpMethod);
Comment on lines +289 to +299

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent a potential NullPointerException when requestBody is null, we should add a defensive null check at the beginning of createRequest.

Suggested change
HttpUriRequestBase createRequest(
String endpoint,
BinaryRequestBody requestBody,
Config config,
boolean isApiKeyRequired,
RequestOptions requestOptions,
ApiConstants.HttpMethod httpMethod,
Map<String, String> params)
throws HTTPClientException {
HttpUriRequestBase httpRequest =
createHttpRequestBase(createUri(endpoint, params), requestBody.getData(), httpMethod);
HttpUriRequestBase createRequest(
String endpoint,
BinaryRequestBody requestBody,
Config config,
boolean isApiKeyRequired,
RequestOptions requestOptions,
ApiConstants.HttpMethod httpMethod,
Map<String, String> params)
throws HTTPClientException {
if (requestBody == null) {
throw new IllegalArgumentException("requestBody cannot be null");
}
HttpUriRequestBase httpRequest =
createHttpRequestBase(createUri(endpoint, params), requestBody.getData(), httpMethod);


RequestConfig.Builder builder = RequestConfig.custom();
builder.setResponseTimeout(config.getReadTimeoutMillis(), TimeUnit.MILLISECONDS);
builder.setConnectTimeout(config.getConnectionTimeoutMillis(), TimeUnit.MILLISECONDS);
builder.setDefaultKeepAlive(config.getDefaultKeepAliveMillis(), TimeUnit.MILLISECONDS);
builder.setConnectionRequestTimeout(
config.getConnectionRequestTimeoutMillis(), TimeUnit.MILLISECONDS);

setContentType(httpUriRequest, APPLICATION_JSON_TYPE);
if (config.getProtocolUpgradeEnabled() != null) {
builder.setProtocolUpgradeEnabled(config.getProtocolUpgradeEnabled());
}
if (proxy != null && proxy.address() instanceof InetSocketAddress) {
InetSocketAddress inetSocketAddress = (InetSocketAddress) proxy.address();
builder.setProxy(new HttpHost(inetSocketAddress.getHostName(), inetSocketAddress.getPort()));
}
httpRequest.setConfig(builder.build());

setAuthentication(httpRequest, isApiKeyRequired, config);
setHeaders(config, requestOptions, httpRequest, requestBody.getContentType());

return httpRequest;
}

private void setHeaders(

Check failure on line 323 in src/main/java/com/adyen/httpclient/AdyenHttpClient.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 18 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Adyen_adyen-java-api-library&issues=AaA0IiQCqCmucr6iXdmf&open=AaA0IiQCqCmucr6iXdmf&pullRequest=2048
Config config,
RequestOptions requestOptions,
HttpUriRequestBase httpUriRequest,
String requestContentType) {

String contentType =
requestContentType != null
? requestContentType
: getAdditionalHeader(requestOptions, CONTENT_TYPE, APPLICATION_JSON_TYPE);
setContentType(httpUriRequest, contentType);
httpUriRequest.addHeader(ACCEPT_CHARSET, CHARSET);

String applicationName = config.getApplicationName();
Expand Down Expand Up @@ -292,11 +358,32 @@
}

if (requestOptions.getAdditionalServiceHeaders() != null) {
requestOptions.getAdditionalServiceHeaders().forEach(httpUriRequest::addHeader);
requestOptions
.getAdditionalServiceHeaders()
.forEach(
(name, value) -> {
if (!name.equalsIgnoreCase(CONTENT_TYPE)
&& (requestContentType == null || !name.equalsIgnoreCase("Content-Length"))) {
httpUriRequest.addHeader(name, value);
}
});
}
}
}

private String getAdditionalHeader(
RequestOptions requestOptions, String name, String defaultValue) {
if (requestOptions != null && requestOptions.getAdditionalServiceHeaders() != null) {
for (Map.Entry<String, String> header :
requestOptions.getAdditionalServiceHeaders().entrySet()) {
if (header.getKey().equalsIgnoreCase(name)) {
return header.getValue();
}
}
}
return defaultValue;
}

private HttpUriRequestBase createHttpRequestBase(
URI endpoint, String requestBody, ApiConstants.HttpMethod httpMethod) {
StringEntity requestEntity = null;
Expand All @@ -321,6 +408,27 @@
}
}

private HttpUriRequestBase createHttpRequestBase(
URI endpoint, byte[] requestBody, ApiConstants.HttpMethod httpMethod) {
ByteArrayEntity requestEntity =
requestBody == null ? null : new ByteArrayEntity(requestBody, null);

switch (httpMethod) {
case GET:
return new HttpGet(endpoint);
case PATCH:
HttpPatch httpPatch = new HttpPatch(endpoint);
httpPatch.setEntity(requestEntity);
return httpPatch;
case DELETE:
return new HttpDelete(endpoint);
default:
HttpPost httpPost = new HttpPost(endpoint);
httpPost.setEntity(requestEntity);
return httpPost;
}
}

private URI createUri(String endpoint, Map<String, String> params) throws HTTPClientException {
try {
URIBuilder uriBuilder = new URIBuilder(endpoint);
Expand Down Expand Up @@ -406,7 +514,7 @@

/** Sets the Content-Type header on the request. */
private void setContentType(HttpUriRequest httpUriRequest, String contentType) {
httpUriRequest.addHeader(CONTENT_TYPE, contentType);
httpUriRequest.setHeader(CONTENT_TYPE, contentType);
}

/** Sets the X-API-Key header on the request. */
Expand Down
58 changes: 58 additions & 0 deletions src/main/java/com/adyen/httpclient/BinaryRequestBody.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* ######
* ######
* ############ ####( ###### #####. ###### ############ ############
* ############# #####( ###### #####. ###### ############# #############
* ###### #####( ###### #####. ###### ##### ###### ##### ######
* ###### ###### #####( ###### #####. ###### ##### ###### ##### ######
* ###### ###### #####( ###### #####. ###### ##### ##### ##### ######
* ############# ############# ############# ############# ##### ######
* ############ ############ ############# ############# ######
* ######
* #############
* ############
*
* Adyen Java API Library
*
* Copyright (c) 2026 Adyen B.V.
* This file is open source and available under the MIT license.
* See the LICENSE file for more info.
*/
package com.adyen.httpclient;

import java.util.Arrays;

/** Contains a binary request payload and its content type. */
public final class BinaryRequestBody {
private final byte[] data;
private final String contentType;

/**
* Creates a binary request payload.
*
* @param data request body data
* @param contentType request content type
*/
public BinaryRequestBody(byte[] data, String contentType) {
this.data = Arrays.copyOf(data, data.length);
this.contentType = contentType;
}
Comment on lines +36 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent a potential NullPointerException when data is null, we should add a defensive null check in the constructor. If data is null, we can default to an empty byte array.

Suggested change
public BinaryRequestBody(byte[] data, String contentType) {
this.data = Arrays.copyOf(data, data.length);
this.contentType = contentType;
}
public BinaryRequestBody(byte[] data, String contentType) {
this.data = data != null ? Arrays.copyOf(data, data.length) : new byte[0];
this.contentType = contentType;
}


/**
* Gets a copy of the request body data.
*
* @return request body data
*/
public byte[] getData() {
return Arrays.copyOf(data, data.length);
}

/**
* Gets the request content type.
*
* @return request content type
*/
public String getContentType() {
return contentType;
}
}
28 changes: 28 additions & 0 deletions src/main/java/com/adyen/httpclient/ClientInterface.java
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,32 @@ String request(
ApiConstants.HttpMethod httpMethod,
Map<String, String> params)
throws IOException, HTTPClientException;

/**
* Sends a binary HTTP request. Custom HTTP client implementations must override this method to
* support multipart API operations.
*
* @param endpoint the full URL of the API endpoint
* @param requestBody the binary request body and its content type
* @param config the client configuration
* @param isApiKeyRequired whether API key authentication is mandatory
* @param requestOptions additional request options (idempotency key, custom headers)
* @param httpMethod the HTTP method
* @param params query string parameters appended to the URL
* @return the response body
* @throws IOException if a network error occurs
* @throws HTTPClientException if the server returns a non-2xx status code
*/
default String requestBinary(
String endpoint,
BinaryRequestBody requestBody,
Config config,
boolean isApiKeyRequired,
RequestOptions requestOptions,
ApiConstants.HttpMethod httpMethod,
Map<String, String> params)
throws IOException, HTTPClientException {
throw new UnsupportedOperationException(
"The configured HTTP client does not support binary request bodies");
}
}
58 changes: 58 additions & 0 deletions src/main/java/com/adyen/httpclient/HttpFile.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* ######
* ######
* ############ ####( ###### #####. ###### ############ ############
* ############# #####( ###### #####. ###### ############# #############
* ###### #####( ###### #####. ###### ##### ###### ##### ######
* ###### ###### #####( ###### #####. ###### ##### ###### ##### ######
* ###### ###### #####( ###### #####. ###### ##### ##### ##### ######
* ############# ############# ############# ############# ##### ######
* ############ ############ ############# ############# ######
* ######
* #############
* ############
*
* Adyen Java API Library
*
* Copyright (c) 2026 Adyen B.V.
* This file is open source and available under the MIT license.
* See the LICENSE file for more info.
*/
package com.adyen.httpclient;

import java.util.Arrays;

/** Represents a file uploaded as part of a multipart request. */
public class HttpFile {
private final byte[] data;
private final String name;

/**
* Creates a file for a multipart request.
*
* @param data file contents
* @param name file name sent to the API
*/
public HttpFile(byte[] data, String name) {
this.data = Arrays.copyOf(data, data.length);
this.name = name;
}
Comment on lines +36 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent a potential NullPointerException when data is null, we should add a defensive null check in the constructor. If data is null, we can default to an empty byte array.

Suggested change
public HttpFile(byte[] data, String name) {
this.data = Arrays.copyOf(data, data.length);
this.name = name;
}
public HttpFile(byte[] data, String name) {
this.data = data != null ? Arrays.copyOf(data, data.length) : new byte[0];
this.name = name;
}


/**
* Gets a copy of the file contents.
*
* @return file contents
*/
public byte[] getData() {
return Arrays.copyOf(data, data.length);
}

/**
* Gets the file name.
*
* @return file name
*/
public String getName() {
return name;
}
}
Loading