Skip to content
Merged
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
3 changes: 2 additions & 1 deletion src/main/java/org/metricshub/http/HttpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
try {
sc = SSLContext.getInstance("SSL");
sc.init(null, LOUSY_TRUST_MANAGER, new java.security.SecureRandom());
} catch (NoSuchAlgorithmException | KeyManagementException e) {}

Check warning on line 112 in src/main/java/org/metricshub/http/HttpClient.java

View workflow job for this annotation

GitHub Actions / PMD

Error Prone EmptyCatchBlock

Avoid empty catch blocks
BASE_SOCKET_FACTORY = sc.getSocketFactory();
DEFAULT_SSL_PROTOCOLS = sc.getDefaultSSLParameters().getProtocols();
}
Expand Down Expand Up @@ -363,7 +363,8 @@
}
}
}
response.appendBody(new String(bodyBytes.toByteArray(), charset));
// Decode straight from the buffer (toByteArray() would copy it first)
response.appendBody(bodyBytes.toString(charset.name()));

// Return
return response;
Expand Down
17 changes: 11 additions & 6 deletions src/main/java/org/metricshub/http/HttpResponse.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
public class HttpResponse {

private int statusCode;
private StringBuilder body;
// The body is kept as a String: a StringBuilder would copy it once when appended and once more in getBody()
private String body;
private StringBuilder header;

/**
Expand All @@ -40,7 +41,7 @@ public class HttpResponse {
public HttpResponse() {
statusCode = 0;
header = new StringBuilder();
body = new StringBuilder();
body = "";
}

/**
Expand Down Expand Up @@ -86,23 +87,27 @@ public void appendHeader(String name, String value) {
* @return the body of the HTTP response
*/
public String getBody() {
return body.toString();
return body;
}

/**
* Append content to the body of the HTTP response
* <p>
* Each call after the first copies the whole body: the body is meant to be set by a single call.
*
* @param data Data to append
* @param data Data to append ("null" is appended for null, as StringBuilder.append() does)
*/
public void appendBody(String data) {
body.append(data);
String value = String.valueOf(data);
body = body.isEmpty() ? value : body.concat(value);
}

/**
* @return the entire HTTP response, header and body
*/
@Override
public String toString() {
return new StringBuilder().append(header).append("\n").append(body).toString();
// concat() copies the body once, where a StringBuilder copies it twice
return header.toString().concat("\n").concat(body);
}
}
138 changes: 138 additions & 0 deletions src/test/java/org/metricshub/http/HttpClientTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package org.metricshub.http;

import static org.junit.jupiter.api.Assertions.assertEquals;

import com.sun.net.httpserver.HttpServer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.zip.GZIPOutputStream;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

/**
* Reading of the response body, against a local server
*/
class HttpClientTest {

private static final String TEXT = "{\"name\":\"café 中 😀\",\"list\":[1,2,3]}";

private static HttpServer server;
private static String baseUrl;

@BeforeAll
static void startServer() throws IOException {
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
respond("/length", 200, "application/json", null, TEXT.getBytes(StandardCharsets.UTF_8), false);
respond("/chunked", 200, "application/json", null, TEXT.getBytes(StandardCharsets.UTF_8), true);
respond(
"/latin1",
200,
"text/plain; charset=ISO-8859-1",
null,
"café".getBytes(StandardCharsets.ISO_8859_1),
false
);
respond("/gzip", 200, "application/json", "gzip", gzip(TEXT.getBytes(StandardCharsets.UTF_8)), true);
respond("/empty", 200, "application/json", null, new byte[0], false);
respond("/error", 404, "text/plain", null, "not found".getBytes(StandardCharsets.UTF_8), false);
server.start();
baseUrl = "http://127.0.0.1:" + server.getAddress().getPort();
}

@AfterAll
static void stopServer() {
server.stop(0);
}

private static void respond(
String path,
int status,
String contentType,
String contentEncoding,
byte[] content,
boolean chunked
) {
server.createContext(
path,
exchange -> {
exchange.getResponseHeaders().add("Content-Type", contentType);
if (contentEncoding != null) {
exchange.getResponseHeaders().add("Content-Encoding", contentEncoding);
}
// Length 0 means chunked, -1 means no body
exchange.sendResponseHeaders(status, chunked ? 0 : content.length == 0 ? -1 : content.length);
try (OutputStream os = exchange.getResponseBody()) {
if (content.length > 0) {
os.write(content);
}
}
}
);
}

private static byte[] gzip(byte[] content) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(out)) {
gzip.write(content);
}
return out.toByteArray();
}

private static HttpResponse get(String path) throws IOException {
return HttpClient.sendRequest(
baseUrl + path,
"GET",
null,
null,
null,
null,
0,
null,
null,
null,
null,
null,
10,
null
);
}

@Test
void testBodyWithContentLength() throws IOException {
HttpResponse response = get("/length");
assertEquals(200, response.getStatusCode());
assertEquals(TEXT, response.getBody());
}

@Test
void testBodyChunked() throws IOException {
assertEquals(TEXT, get("/chunked").getBody());
}

@Test
void testBodyCharsetFromContentType() throws IOException {
assertEquals("café", get("/latin1").getBody());
}

@Test
void testBodyGzip() throws IOException {
assertEquals(TEXT, get("/gzip").getBody());
}

@Test
void testBodyEmpty() throws IOException {
assertEquals("", get("/empty").getBody());
}

@Test
void testBodyOfError() throws IOException {
HttpResponse response = get("/error");
assertEquals(404, response.getStatusCode());
assertEquals("not found", response.getBody());
assertEquals(response.getHeader() + "\nnot found", response.toString());
}
}
11 changes: 11 additions & 0 deletions src/test/java/org/metricshub/http/HttpResponseTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.metricshub.http;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -35,6 +36,16 @@ void testGetBody() {
r.appendBody("abc");
r.appendBody("def");
assertEquals("abcdef", r.getBody());
r.appendBody(null);
r.appendBody("");
assertEquals("abcdefnull", r.getBody());

// The body is not copied
HttpResponse single = new HttpResponse();
String body = "single";
single.appendBody(body);
assertSame(body, single.getBody());
assertSame(single.getBody(), single.getBody());
}

@Test
Expand Down
Loading