/
githubmirror
/
galahad
Обзор
Документация
Войти
/
githubmirror
/
galahad
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/java.net.http/share/classes/jdk/internal/net/http/RequestPublishers.java
751 строка
26 KB
Volkan Yazici
8367067: Improve exception handling in HttpRequest.BodyPublishers
19 сен 2025, 15:07
19 сен 2025, 15:07
87d5042
Код
Авторство
О чём код?
/* * Copyright (c) 2016, 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. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * 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. */ package jdk.internal.net.http; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; import java.lang.reflect.UndeclaredThrowableException; import java.net.http.HttpRequest.BodyPublisher; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Flow; import java.util.concurrent.Flow.Publisher; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import jdk.internal.net.http.common.Demand; import jdk.internal.net.http.common.SequentialScheduler; import jdk.internal.net.http.common.Utils; public final class RequestPublishers { private RequestPublishers() { } public static class ByteArrayPublisher implements BodyPublisher { private final int length; private final byte[] content; private final int offset; private final int bufSize; public ByteArrayPublisher(byte[] content) { this(content, 0, content.length); } public ByteArrayPublisher(byte[] content, int offset, int length) { this(content, offset, length, Utils.BUFSIZE); } private ByteArrayPublisher(byte[] content, int offset, int length, int bufSize) { Objects.checkFromIndexSize(offset, length, content.length); // Implicit null check on `content` if (bufSize <= 0) { throw new IllegalArgumentException("Invalid buffer size: " + bufSize); } this.content = content; this.offset = offset; this.length = length; this.bufSize = bufSize; } List<ByteBuffer> copy(byte[] content, int offset, int length) { List<ByteBuffer> bufs = new ArrayList<>(); while (length > 0) { ByteBuffer b = ByteBuffer.allocate(Math.min(bufSize, length)); int max = b.capacity(); int tocopy = Math.min(max, length); b.put(content, offset, tocopy); offset += tocopy; length -= tocopy; b.flip(); bufs.add(b); } return bufs; } @Override public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) { List<ByteBuffer> copy = copy(content, offset, length); var delegate = new PullPublisher<>(CheckedIterable.fromIterable(copy)); delegate.subscribe(subscriber); } @Override public long contentLength() { return length; } } // This implementation has lots of room for improvement. public static class IterablePublisher implements BodyPublisher { private final Iterable<byte[]> content; private volatile long contentLength; public IterablePublisher(Iterable<byte[]> content) { this.content = Objects.requireNonNull(content); } // The ByteBufferIterator will iterate over the byte[] arrays in // the content one at the time. // private final class ByteBufferIterator implements CheckedIterator<ByteBuffer> { final ConcurrentLinkedQueue<ByteBuffer> buffers = new ConcurrentLinkedQueue<>(); final Iterator<byte[]> iterator = content.iterator(); @Override public boolean hasNext() { return !buffers.isEmpty() || iterator.hasNext(); } @Override public ByteBuffer next() { ByteBuffer buffer = buffers.poll(); while (buffer == null) { copy(); buffer = buffers.poll(); } return buffer; } ByteBuffer getBuffer() { return Utils.getBuffer(); } void copy() { byte[] bytes = iterator.next(); int length = bytes.length; if (length == 0 && iterator.hasNext()) { // avoid inserting empty buffers, except // if that's the last. return; } int offset = 0; do { ByteBuffer b = getBuffer(); int max = b.capacity(); int tocopy = Math.min(max, length); b.put(bytes, offset, tocopy); offset += tocopy; length -= tocopy; b.flip(); buffers.add(b); } while (length > 0); } } @Override public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) { CheckedIterable<ByteBuffer> iterable = () -> new ByteBufferIterator(); var delegate = new PullPublisher<>(iterable); delegate.subscribe(subscriber); } static long computeLength(Iterable<byte[]> bytes) { // Avoid iterating just for the purpose of computing // a length, in case iterating is a costly operation // For HTTP/1.1 it means we will be using chunk encoding // when sending the request body. // For HTTP/2 it means we will not send the optional // Content-length header. return -1; } @Override public long contentLength() { if (contentLength == 0) { synchronized (this) { if (contentLength == 0) { contentLength = computeLength(content); } } } return contentLength; } } public static class StringPublisher extends ByteArrayPublisher { public StringPublisher(String content, Charset charset) { super(content.getBytes(Objects.requireNonNull(charset))); // Implicit null check on `content` } } public static class EmptyPublisher implements BodyPublisher { private final Flow.Publisher<ByteBuffer> delegate = new PullPublisher<>(CheckedIterable.fromIterable(Collections.emptyList()), null); @Override public long contentLength() { return 0; } @Override public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) { delegate.subscribe(subscriber); } } /** * Publishes the content of a given file. */ public static class FilePublisher implements BodyPublisher { private final Path path; private final long length; /** * Factory for creating FilePublisher. */ public static FilePublisher create(Path path) throws FileNotFoundException { if (Files.notExists(path)) throw new FileNotFoundException(path + " not found"); long length; try { length = Files.size(path); } catch (IOException ioe) { length = -1; } return new FilePublisher(path, length); } private FilePublisher(Path name, long length) { path = name; this.length = length; } @Override public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) { InputStream is = null; Throwable t = null; try { // Throw `FileNotFoundException` to match the specification of `BodyPublishers::ofFile if (!Files.isRegularFile(path)) { throw new FileNotFoundException(path + " (Not a regular file)"); } is = Files.newInputStream(path); } catch (NoSuchFileException nsfe) { // Throw `FileNotFoundException` to match the specification of `BodyPublishers::ofFile` t = new FileNotFoundException(path + " (No such file or directory)"); } catch (UncheckedIOException | UndeclaredThrowableException ue) { t = ue.getCause(); } catch (Throwable th) { t = th; } final InputStream fis = is; PullPublisher<ByteBuffer> publisher; if (t == null) { publisher = new PullPublisher<>(() -> new StreamIterator(fis)); } else { publisher = new PullPublisher<>(null, t); } publisher.subscribe(subscriber); } @Override public long contentLength() { return length; } } /** * Reads one buffer ahead all the time, blocking in hasNext() */ private static final class StreamIterator implements CheckedIterator<ByteBuffer> { final InputStream is; final Supplier<? extends ByteBuffer> bufSupplier; private volatile boolean eof; volatile ByteBuffer nextBuffer; volatile boolean need2Read = true; volatile boolean haveNext; final ReentrantLock stateLock = new ReentrantLock(); StreamIterator(InputStream is) { this(is, Utils::getBuffer); } StreamIterator(InputStream is, Supplier<? extends ByteBuffer> bufSupplier) { this.is = is; this.bufSupplier = bufSupplier; } // Throwable error() { // return error; // } private int read() throws IOException { if (eof) return -1; nextBuffer = bufSupplier.get(); nextBuffer.clear(); byte[] buf = nextBuffer.array(); int offset = nextBuffer.arrayOffset(); int cap = nextBuffer.capacity(); int n = is.read(buf, offset, cap); if (n == -1) { eof = true; return -1; } //flip nextBuffer.limit(n); nextBuffer.position(0); return n; } @Override public boolean hasNext() throws IOException { stateLock.lock(); try { return hasNext0(); } finally { stateLock.unlock(); } } private boolean hasNext0() throws IOException { if (need2Read) { try { haveNext = read() != -1; if (haveNext) { need2Read = false; } } catch (IOException e) { haveNext = false; need2Read = false; throw e; } finally { if (!haveNext) { is.close(); } } } return haveNext; } @Override public ByteBuffer next() throws IOException { stateLock.lock(); try { if (!hasNext()) { throw new NoSuchElementException(); } need2Read = true; return nextBuffer; } finally { stateLock.unlock(); } } } public static class InputStreamPublisher implements BodyPublisher { private final Supplier<? extends InputStream> streamSupplier; public InputStreamPublisher(Supplier<? extends InputStream> streamSupplier) { this.streamSupplier = Objects.requireNonNull(streamSupplier); } @Override public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) { InputStream is = null; Exception exception = null; try { is = streamSupplier.get(); if (is == null) { exception = new IOException("Stream supplier returned null"); } } catch (Exception cause) { exception = new IOException("Stream supplier has failed", cause); } PullPublisher<ByteBuffer> publisher = exception != null ? new PullPublisher<>(null, exception) : new PullPublisher<>(iterableOf(is), null); publisher.subscribe(subscriber); } private CheckedIterable<ByteBuffer> iterableOf(InputStream is) { return () -> new StreamIterator(is); } @Override public long contentLength() { return -1; } } public static final class FileChannelPublisher implements BodyPublisher { private final FileChannel channel; private final long position; private final long limit; public FileChannelPublisher(FileChannel channel, long offset, long length) throws IOException { this.channel = Objects.requireNonNull(channel, "channel"); long fileSize = channel.size(); Objects.checkFromIndexSize(offset, length, fileSize); this.position = offset; this.limit = offset + length; } @Override public long contentLength() { return limit - position; } @Override public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) { CheckedIterable<ByteBuffer> iterable = () -> new FileChannelIterator(channel, position, limit); new PullPublisher<>(iterable).subscribe(subscriber); } } private static final class FileChannelIterator implements CheckedIterator<ByteBuffer> { private final FileChannel channel; private final long limit; private long position; private boolean terminated; private FileChannelIterator(FileChannel channel, long position, long limit) { this.channel = channel; this.position = position; this.limit = limit; } @Override public boolean hasNext() { return position < limit && !terminated; } @Override public ByteBuffer next() throws IOException { if (!hasNext()) { throw new NoSuchElementException(); } long remaining = limit - position; ByteBuffer buffer = Utils.getBufferWithAtMost(remaining); try { int readLength = channel.read(buffer, position); // Short-circuit if `read()` has failed, e.g., due to file content being changed in the meantime if (readLength < 0) { // Throw to signal that the request needs to be cancelled throw new IOException("Unexpected EOF (position=%s)".formatted(position)); } else { position += readLength; } } catch (IOException ioe) { terminated = true; throw ioe; } return buffer.flip(); } } public static final class PublisherAdapter implements BodyPublisher { private final Publisher<? extends ByteBuffer> publisher; private final long contentLength; public PublisherAdapter(Publisher<? extends ByteBuffer> publisher, long contentLength) { this.publisher = Objects.requireNonNull(publisher); this.contentLength = contentLength; } @Override public long contentLength() { return contentLength; } @Override public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) { publisher.subscribe(subscriber); } } public static BodyPublisher concat(BodyPublisher... publishers) { if (publishers.length == 0) { return new EmptyPublisher(); } else if (publishers.length == 1) { return Objects.requireNonNull(publishers[0]); } else { return new AggregatePublisher(List.of(publishers)); } } /** * An aggregate publisher acts as a proxy between a subscriber * and a list of publishers. It lazily subscribes to each publisher * in sequence in order to publish a request body that is * composed from all the bytes obtained from each publisher. * For instance, the following two publishers are equivalent, even * though they may result in a different count of {@code onNext} * invocations. * <pre>{@code * var bp1 = BodyPublishers.ofString("ab"); * var bp2 = BodyPublishers.concat(BodyPublishers.ofString("a"), * BodyPublisher.ofByteArray(new byte[] {(byte)'b'})); * }</pre> * */ private static final class AggregatePublisher implements BodyPublisher { final List<BodyPublisher> bodies; AggregatePublisher(List<BodyPublisher> bodies) { this.bodies = bodies; } // -1 must be returned if any publisher returns -1 // Otherwise, we can just sum the contents. @Override public long contentLength() { long length = bodies.stream() .mapToLong(BodyPublisher::contentLength) .reduce((a,b) -> a < 0 || b < 0 ? -1 : a + b) .orElse(0); // In case of overflow in any operation but the last, length // will be -1. // In case of overflow in the last reduce operation, length // will be negative, but not necessarily -1: in that case, // return -1 if (length < 0) return -1; return length; } @Override public void subscribe(Flow.Subscriber<? super ByteBuffer> subscriber) { subscriber.onSubscribe(new AggregateSubscription(bodies, subscriber)); } } private static final class AggregateSubscription implements Flow.Subscription, Flow.Subscriber<ByteBuffer> { final Flow.Subscriber<? super ByteBuffer> subscriber; // upstream final Queue<BodyPublisher> bodies; final SequentialScheduler scheduler; final Demand demand = new Demand(); // from upstream final Demand demanded = new Demand(); // requested downstream final AtomicReference<Throwable> error = new AtomicReference<>(); volatile Throwable illegalRequest; volatile BodyPublisher publisher; // downstream volatile Flow.Subscription subscription; // downstream volatile boolean cancelled; AggregateSubscription(List<BodyPublisher> bodies, Flow.Subscriber<? super ByteBuffer> subscriber) { this.bodies = new ConcurrentLinkedQueue<>(bodies); this.subscriber = subscriber; this.scheduler = SequentialScheduler.lockingScheduler(this::run); } @Override public void request(long n) { synchronized (this) { // We are finished when publisher is null and bodies // is empty. This means that the data from the last // publisher in the list has been consumed. // If we are finished or cancelled, do nothing. if (cancelled || (publisher == null && bodies.isEmpty())) { return; } } try { demand.increase(n); } catch (IllegalArgumentException x) { // request() should not throw - the scheduler will // invoke onError on the subscriber. illegalRequest = x; } scheduler.runOrSchedule(); } @Override public void cancel() { cancelled = true; scheduler.runOrSchedule(); } private boolean cancelSubscription(Flow.Subscription subscription) { if (subscription != null) { synchronized (this) { if (this.subscription == subscription) { this.subscription = null; this.publisher = null; } } subscription.cancel(); } // This method is called when cancel is true, so // we should always stop the scheduler here scheduler.stop(); return subscription != null; } public void run() { try { BodyPublisher publisher; Flow.Subscription subscription = null; while (error.get() == null && (!demand.isFulfilled() || (this.publisher == null && !bodies.isEmpty()))) { boolean cancelled = this.cancelled; // make sure we see a consistent state. synchronized (this) { publisher = this.publisher; subscription = this.subscription; } Throwable illegalRequest = this.illegalRequest; if (cancelled) { bodies.clear(); cancelSubscription(subscription); return; } if (publisher == null && !bodies.isEmpty()) { // synchronize here to avoid race condition with // request(long) which could otherwise observe a // null publisher and an empty bodies list when // polling the last publisher. synchronized (this) { this.publisher = publisher = bodies.poll(); } publisher.subscribe(this); } else if (publisher == null) { return; } if (illegalRequest != null) { onError(illegalRequest); return; } long n = 0; // synchronize to avoid race condition with // publisherDone() synchronized (this) { if ((subscription = this.subscription) == null) return; if (!demand.isFulfilled()) { n = demand.decreaseAndGet(demand.get()); demanded.increase(n); } } if (n > 0 && !cancelled) { subscription.request(n); } } } catch (Throwable t) { onError(t); } } // It is important to synchronize when setting // publisher to null to avoid race conditions // with request(long) private synchronized void publisherDone() { publisher = null; subscription = null; } @Override public void onSubscribe(Flow.Subscription subscription) { // synchronize for asserting in a consistent state. synchronized (this) { // we shouldn't be able to observe a null publisher // when onSubscribe is called, unless - possibly - if // there was some error... assert publisher != null || error.get() != null; this.subscription = subscription; } scheduler.runOrSchedule(); } @Override public void onNext(ByteBuffer item) { // make sure to cancel the downstream subscription if we receive // an item after the aggregate subscription was cancelled or // an error was reported. if (cancelled || error.get() != null) { cancelSubscription(this.subscription); return; } demanded.tryDecrement(); subscriber.onNext(item); } @Override public void onError(Throwable throwable) { if (error.compareAndSet(null, throwable)) { publisherDone(); subscriber.onError(throwable); scheduler.stop(); } } private synchronized boolean completeAndContinue() { if (publisher != null && !bodies.isEmpty()) { while (!demanded.isFulfilled()) { demand.increase(demanded.decreaseAndGet(demanded.get())); } publisherDone(); return true; // continue } else { publisherDone(); return false; // stop } } @Override public void onComplete() { if (completeAndContinue()) { scheduler.runOrSchedule(); } else { if (!cancelled) { subscriber.onComplete(); } scheduler.stop(); } } } }