| /* | |
| * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.OutputStream; | |
| import java.io.UncheckedIOException; | |
| import java.net.InetAddress; | |
| import java.net.InetSocketAddress; | |
| import java.net.ServerSocket; | |
| import java.net.Socket; | |
| import java.net.SocketException; | |
| import java.net.URI; | |
| import java.net.http.HttpClient; | |
| import java.net.http.HttpRequest; | |
| import java.net.http.HttpRequest.BodyPublishers; | |
| import java.net.http.HttpResponse; | |
| import java.net.http.HttpResponse.BodyHandlers; | |
| import java.net.http.HttpTimeoutException; | |
| import java.nio.file.Files; | |
| import java.nio.file.Path; | |
| import java.nio.file.Paths; | |
| import java.nio.ByteBuffer; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.concurrent.CompletableFuture; | |
| import java.util.concurrent.ExecutionException; | |
| import java.util.concurrent.Flow; | |
| import java.util.concurrent.TimeoutException; | |
| import java.util.concurrent.TimeUnit; | |
| import java.util.function.Supplier; | |
| import static java.lang.System.err; | |
| import static java.nio.charset.StandardCharsets.US_ASCII; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| /** | |
| * @test | |
| * @bug 8151441 | |
| * @summary Request body of incorrect (larger or smaller) sizes than that | |
| * reported by the body publisher | |
| * @run main/othervm ShortRequestBody | |
| */ | |
| public class ShortRequestBody { | |
| static final Path testSrc = Paths.get(System.getProperty("test.src", ".")); | |
| // Some body types ( sources ) for testing. | |
| static final String STRING_BODY = "Hello world"; | |
| static final byte[] BYTE_ARRAY_BODY = new byte[] { | |
| (byte)0xCA, (byte)0xFE, (byte)0xBA, (byte)0xBE }; | |
| static final Path FILE_BODY = testSrc.resolve("docs").resolve("files").resolve("foo.txt"); | |
| // Body lengths and offsets ( amount to be wrong by ), to make coordination | |
| // between client and server easier. | |
| static final int[] BODY_LENGTHS = new int[] { STRING_BODY.length(), | |
| BYTE_ARRAY_BODY.length, | |
| fileSize(FILE_BODY) }; | |
| static final int[] BODY_OFFSETS = new int[] { 0, +1, -1, +2, -2, +3, -3 }; | |
| static final String MARKER = "ShortRequestBody"; | |
| // A delegating Body Publisher. Subtypes will have a concrete body type. | |
| static abstract class AbstractDelegateRequestBody | |
| implements HttpRequest.BodyPublisher { | |
| final HttpRequest.BodyPublisher delegate; | |
| final long contentLength; | |
| AbstractDelegateRequestBody(HttpRequest.BodyPublisher delegate, | |
| long contentLength) { | |
| this.delegate = delegate; | |
| this.contentLength = contentLength; | |
| } | |
| @Override | |
| public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) { | |
| delegate.subscribe(subscriber); | |
| } | |
| @Override | |
| public long contentLength() { return contentLength; /* may be wrong! */ } | |
| } | |
| // Request body Publishers that may generate a different number of actual | |
| // bytes to that of what is reported through their {@code contentLength}. | |
| static class StringRequestBody extends AbstractDelegateRequestBody { | |
| StringRequestBody(String body, int additionalLength) { | |
| super(HttpRequest.BodyPublishers.ofString(body), | |
| body.getBytes(UTF_8).length + additionalLength); | |
| } | |
| } | |
| static class ByteArrayRequestBody extends AbstractDelegateRequestBody { | |
| ByteArrayRequestBody(byte[] body, int additionalLength) { | |
| super(BodyPublishers.ofByteArray(body), | |
| body.length + additionalLength); | |
| } | |
| } | |
| static class FileRequestBody extends AbstractDelegateRequestBody { | |
| FileRequestBody(Path path, int additionalLength) throws IOException { | |
| super(BodyPublishers.ofFile(path), | |
| Files.size(path) + additionalLength); | |
| } | |
| } | |
| // --- | |
| public static void main(String[] args) throws Exception { | |
| HttpClient sharedClient = HttpClient.newHttpClient(); | |
| List<Supplier<HttpClient>> clientSuppliers = new ArrayList<>(); | |
| clientSuppliers.add(() -> HttpClient.newHttpClient()); | |
| clientSuppliers.add(() -> sharedClient); | |
| try (Server server = new Server()) { | |
| for (Supplier<HttpClient> cs : clientSuppliers) { | |
| err.println("\n---- next supplier ----\n"); | |
| URI uri = new URI("http://localhost:" + server.getPort() + "/" + MARKER); | |
| // sanity ( 6 requests to keep client and server offsets easy to workout ) | |
| success(cs, uri, new StringRequestBody(STRING_BODY, 0)); | |
| success(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, 0)); | |
| success(cs, uri, new FileRequestBody(FILE_BODY, 0)); | |
| success(cs, uri, new StringRequestBody(STRING_BODY, 0)); | |
| success(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, 0)); | |
| success(cs, uri, new FileRequestBody(FILE_BODY, 0)); | |
| for (int i = 1; i < BODY_OFFSETS.length; i++) { | |
| failureBlocking(cs, uri, new StringRequestBody(STRING_BODY, BODY_OFFSETS[i])); | |
| failureBlocking(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, BODY_OFFSETS[i])); | |
| failureBlocking(cs, uri, new FileRequestBody(FILE_BODY, BODY_OFFSETS[i])); | |
| failureNonBlocking(cs, uri, new StringRequestBody(STRING_BODY, BODY_OFFSETS[i])); | |
| failureNonBlocking(cs, uri, new ByteArrayRequestBody(BYTE_ARRAY_BODY, BODY_OFFSETS[i])); | |
| failureNonBlocking(cs, uri, new FileRequestBody(FILE_BODY, BODY_OFFSETS[i])); | |
| } | |
| } | |
| } | |
| } | |
| static void success(Supplier<HttpClient> clientSupplier, | |
| URI uri, | |
| HttpRequest.BodyPublisher publisher) | |
| throws Exception | |
| { | |
| CompletableFuture<HttpResponse<Void>> cf; | |
| HttpRequest request = HttpRequest.newBuilder(uri) | |
| .POST(publisher) | |
| .build(); | |
| cf = clientSupplier.get().sendAsync(request, BodyHandlers.discarding()); | |
| HttpResponse<Void> resp = cf.get(30, TimeUnit.SECONDS); | |
| err.println("Response code: " + resp.statusCode()); | |
| check(resp.statusCode() == 200, null, | |
| "Expected 200, got ", resp.statusCode()); | |
| } | |
| static void checkCause(Throwable cause) { | |
| Throwable t = cause; | |
| boolean found = false; | |
| while (t != null && !found) { | |
| check(t instanceof IOException, t, | |
| "Expected cause IOException, but got: ", t); | |
| String msg = t.getMessage(); | |
| found = (msg != null && (msg.contains("Too many") || msg.contains("Too few"))); | |
| t = t.getCause(); | |
| } | |
| if (!found) { | |
| String msg = cause.getMessage(); | |
| check(msg.contains("Too many") || msg.contains("Too few"), | |
| cause, "Expected Too many|Too few, got: ", cause); | |
| } | |
| } | |
| static void failureNonBlocking(Supplier<HttpClient> clientSupplier, | |
| URI uri, | |
| HttpRequest.BodyPublisher publisher) | |
| throws Exception | |
| { | |
| CompletableFuture<HttpResponse<Void>> cf; | |
| HttpRequest request = HttpRequest.newBuilder(uri) | |
| .POST(publisher) | |
| .build(); | |
| cf = clientSupplier.get().sendAsync(request, BodyHandlers.discarding()); | |
| try { | |
| HttpResponse<Void> r = cf.get(30, TimeUnit.SECONDS); | |
| throw new RuntimeException("Unexpected response: " + r.statusCode()); | |
| } catch (TimeoutException x) { | |
| throw new RuntimeException("Unexpected timeout", x); | |
| } catch (ExecutionException expected) { | |
| err.println("Caught expected: " + expected); | |
| checkCause(expected.getCause()); | |
| } | |
| } | |
| static void failureBlocking(Supplier<HttpClient> clientSupplier, | |
| URI uri, | |
| HttpRequest.BodyPublisher publisher) | |
| throws Exception | |
| { | |
| HttpRequest request = HttpRequest.newBuilder(uri) | |
| .POST(publisher) | |
| .build(); | |
| try { | |
| HttpResponse<Void> r = clientSupplier.get() | |
| .send(request, BodyHandlers.discarding()); | |
| throw new RuntimeException("Unexpected response: " + r.statusCode()); | |
| } catch (HttpTimeoutException x) { | |
| throw new RuntimeException("Unexpected timeout", x); | |
| } catch (IOException expected) { | |
| err.println("Caught expected: " + expected); | |
| checkCause(expected); | |
| } | |
| } | |
| static class Server extends Thread implements AutoCloseable { | |
| static String RESPONSE = "HTTP/1.1 200 OK\r\n" + | |
| "Connection: close\r\n"+ | |
| "Content-length: 0\r\n\r\n"; | |
| private final ServerSocket ss; | |
| private volatile boolean closed; | |
| Server() throws IOException { | |
| super("Test-Server"); | |
| ss = new ServerSocket(); | |
| ss.setReuseAddress(false); | |
| ss.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)); | |
| this.start(); | |
| } | |
| int getPort() { return ss.getLocalPort(); } | |
| @Override | |
| public void run() { | |
| int count = 0; | |
| int offset = 0; | |
| while (!closed) { | |
| err.println("Server: waiting for connection"); | |
| try (Socket s = ss.accept()) { | |
| err.println("Server: got connection"); | |
| InputStream is = s.getInputStream(); | |
| try { | |
| String headers = readRequestHeaders(is); | |
| if (headers == null) continue; | |
| } catch (SocketException ex) { | |
| err.println("Ignoring unexpected exception while reading headers: " + ex); | |
| ex.printStackTrace(err); | |
| // proceed in order to update count etc..., even though | |
| // we know that read() will fail; | |
| } | |
| byte[] ba = new byte[1024]; | |
| int length = BODY_LENGTHS[count % 3]; | |
| length += BODY_OFFSETS[offset]; | |
| err.println("Server: count=" + count + ", offset=" + offset); | |
| err.println("Server: expecting " +length+ " bytes"); | |
| int read = 0; | |
| try { | |
| read = is.readNBytes(ba, 0, length); | |
| err.println("Server: actually read " + read + " bytes"); | |
| } finally { | |
| // Update the counts before replying, to prevent the | |
| // client-side racing reset with this thread. | |
| count++; | |
| if (count % 6 == 0) // 6 is the number of failure requests per offset | |
| offset++; | |
| if (count % 42 == 0) { | |
| count = 0; // reset, for second iteration | |
| offset = 0; | |
| } | |
| } | |
| if (read < length) { | |
| // no need to reply, client has already closed | |
| // ensure closed | |
| if (is.read() != -1) | |
| new AssertionError("Unexpected read: " + read); | |
| } else { | |
| OutputStream os = s.getOutputStream(); | |
| err.println("Server: writing " | |
| + RESPONSE.getBytes(US_ASCII).length + " bytes"); | |
| os.write(RESPONSE.getBytes(US_ASCII)); | |
| } | |
| } catch (Throwable e) { | |
| if (!closed) { | |
| err.println("Unexpected: " + e); | |
| e.printStackTrace(); | |
| } | |
| } | |
| } | |
| } | |
| @Override | |
| public void close() { | |
| if (closed) | |
| return; | |
| closed = true; | |
| try { | |
| ss.close(); | |
| } catch (IOException e) { | |
| throw new UncheckedIOException("Unexpected", e); | |
| } | |
| } | |
| } | |
| static final byte[] requestEnd = new byte[] {'\r', '\n', '\r', '\n' }; | |
| // Read until the end of a HTTP request headers | |
| static String readRequestHeaders(InputStream is) throws IOException { | |
| int requestEndCount = 0, r, eol = -1; | |
| StringBuilder headers = new StringBuilder(); | |
| while ((r = is.read()) != -1) { | |
| if (r == '\r' && eol < 0) { | |
| eol = headers.length(); | |
| } | |
| headers.append((char) r); | |
| if (r == requestEnd[requestEndCount]) { | |
| requestEndCount++; | |
| if (requestEndCount == 4) { | |
| break; | |
| } | |
| } else { | |
| requestEndCount = 0; | |
| } | |
| } | |
| if (eol <= 0) return null; | |
| String requestLine = headers.toString().substring(0, eol); | |
| if (!requestLine.contains(MARKER)) return null; | |
| return headers.toString(); | |
| } | |
| static int fileSize(Path p) { | |
| try { return (int) Files.size(p); } | |
| catch (IOException x) { throw new UncheckedIOException(x); } | |
| } | |
| static boolean check(boolean cond, Throwable t, Object... failedArgs) { | |
| if (cond) | |
| return true; | |
| // We are going to fail... | |
| StringBuilder sb = new StringBuilder(); | |
| for (Object o : failedArgs) | |
| sb.append(o); | |
| throw new RuntimeException(sb.toString(), t); | |
| } | |
| } |