1212import java .nio .charset .CharacterCodingException ;
1313import java .nio .charset .CharsetDecoder ;
1414import java .nio .charset .CoderResult ;
15+ import java .nio .charset .CodingErrorAction ;
1516import java .nio .charset .StandardCharsets ;
1617import java .util .ArrayList ;
1718import java .util .List ;
@@ -180,12 +181,25 @@ static <T> Flux<T> drain(Publisher<List<ByteBuffer>> body) {
180181
181182 /**
182183 * Stateful UTF-8 decoder that splits a stream of byte-buffer chunks into complete
183- * lines. Handles multi-byte characters split across chunk boundaries, and both
184- * {@code "\n"} and {@code "\r\n"} terminators.
184+ * lines. Handles multi-byte characters split across chunk boundaries, and terminates
185+ * a line on {@code "\r\n"}, {@code "\r"} or {@code "\n"} alike, as the SSE wire
186+ * format does. Bytes that do not decode are replaced rather than reported, so a peer
187+ * sending one does not cost the stream.
185188 */
186189 static final class Utf8LineDecoder {
187190
188- private final CharsetDecoder decoder = StandardCharsets .UTF_8 .newDecoder ();
191+ /**
192+ * Undecodable input costs one replacement character rather than the stream: a
193+ * decoder left on the default {@link CodingErrorAction#REPORT} fails the whole
194+ * response over a single byte a peer mangled, and takes with it the lines already
195+ * decoded from the same chunk, because {@link #decode(List)} throws instead of
196+ * returning them. A body cut short mid-character is enough to hit it. This
197+ * matches {@link java.net.http.HttpResponse.BodySubscribers#fromLineSubscriber},
198+ * the path this decoder replaces, which configured the same two actions.
199+ */
200+ private final CharsetDecoder decoder = StandardCharsets .UTF_8 .newDecoder ()
201+ .onMalformedInput (CodingErrorAction .REPLACE )
202+ .onUnmappableCharacter (CodingErrorAction .REPLACE );
189203
190204 private final CharBuffer charBuffer = CharBuffer .allocate (4096 );
191205
@@ -202,6 +216,14 @@ static final class Utf8LineDecoder {
202216 */
203217 private int scannedForLineTerminator = 0 ;
204218
219+ /**
220+ * Whether the line just emitted was terminated by a CR, so that a LF opening what
221+ * follows completes that terminator instead of ending a line of its own. A CR is
222+ * emitted on as soon as it arrives, before it is known whether a LF follows it,
223+ * and the two may be split across chunks.
224+ */
225+ private boolean crTerminatedPreviousLine = false ;
226+
205227 // Holds partial UTF-8 sequences left over from a previous chunk (max 3 bytes
206228 // for a BMP code point; 4 bytes for a supplementary one).
207229 private ByteBuffer pendingBytes = ByteBuffer .allocate (0 );
@@ -221,6 +243,9 @@ List<String> decode(List<ByteBuffer> chunk) {
221243 CoderResult result = decoder .decode (input , charBuffer , false );
222244 drainCharBuffer ();
223245 extractCompletedLines (lines );
246+ // Unreachable while the decoder replaces undecodable input, but kept
247+ // so that an error result cannot spin this loop: it is neither an
248+ // underflow nor an overflow.
224249 if (result .isError ()) {
225250 try {
226251 result .throwException ();
@@ -270,13 +295,11 @@ List<String> flush() {
270295 extractCompletedLines (lines );
271296 if (leftover .length () > 0 ) {
272297 String last = leftover .toString ();
273- if (last .endsWith ("\r " )) {
274- last = last .substring (0 , last .length () - 1 );
275- }
276298 leftover .setLength (0 );
277299 this .scannedForLineTerminator = 0 ;
278300 lines .add (last );
279301 }
302+ this .crTerminatedPreviousLine = false ;
280303 return lines ;
281304 }
282305
@@ -287,19 +310,43 @@ private void drainCharBuffer() {
287310 }
288311
289312 private void extractCompletedLines (List <String > out ) {
290- int newlineIdx ;
291- while ((newlineIdx = leftover .indexOf ("\n " , this .scannedForLineTerminator )) != -1 ) {
292- String line = leftover .substring (0 , newlineIdx );
293- if (line .endsWith ("\r " )) {
294- line = line .substring (0 , line .length () - 1 );
313+ while (true ) {
314+ if (this .crTerminatedPreviousLine ) {
315+ if (leftover .length () == 0 ) {
316+ // The LF, if there is one, is in a chunk that has not arrived.
317+ return ;
318+ }
319+ if (leftover .charAt (0 ) == '\n' ) {
320+ leftover .delete (0 , 1 );
321+ }
322+ this .crTerminatedPreviousLine = false ;
295323 }
296- out .add (line );
297- leftover .delete (0 , newlineIdx + 1 );
324+ int terminatorIdx = indexOfLineTerminator (this .scannedForLineTerminator );
325+ if (terminatorIdx == -1 ) {
326+ this .scannedForLineTerminator = leftover .length ();
327+ return ;
328+ }
329+ out .add (leftover .substring (0 , terminatorIdx ));
330+ this .crTerminatedPreviousLine = leftover .charAt (terminatorIdx ) == '\r' ;
331+ leftover .delete (0 , terminatorIdx + 1 );
298332 // What is left starts after the terminator, so none of it has been
299333 // searched yet.
300334 this .scannedForLineTerminator = 0 ;
301335 }
302- this .scannedForLineTerminator = leftover .length ();
336+ }
337+
338+ /**
339+ * Index of the first CR or LF in {@link #leftover} at or after {@code from}, or
340+ * {@code -1} when there is none.
341+ */
342+ private int indexOfLineTerminator (int from ) {
343+ for (int i = from ; i < leftover .length (); i ++) {
344+ char c = leftover .charAt (i );
345+ if (c == '\n' || c == '\r' ) {
346+ return i ;
347+ }
348+ }
349+ return -1 ;
303350 }
304351
305352 }
@@ -479,7 +526,7 @@ public void onComplete() {
479526
480527 /**
481528 * A {@link BoundedBodySubscriber} that aborts the response once a single line (a run
482- * of bytes with no LF ) exceeds {@code maxSize} bytes.
529+ * of bytes with no line terminator ) exceeds {@code maxSize} bytes.
483530 *
484531 * <p>
485532 * {@link Utf8LineDecoder} buffers characters until it encounters a line terminator,
@@ -488,9 +535,10 @@ public void onComplete() {
488535 * wire and cancels the subscription before that buffer can grow without bound.
489536 *
490537 * <p>
491- * Only LF resets the count, because LF is the only byte {@link Utf8LineDecoder}
492- * flushes a line on: a lone CR leaves the decoder's buffer growing, so it must not
493- * refill this budget either. CRLF still resets, on its LF.
538+ * CR and LF both reset the count, matching the terminators {@link Utf8LineDecoder}
539+ * flushes a line on: whatever empties the decoder's buffer has to refill this budget,
540+ * or a peer framing short lines with CR alone would be aborted for exceeding a bound
541+ * its lines never reach. A CRLF resets twice, which is harmless.
494542 */
495543 static final class BoundedLineBodySubscriber <T > extends BoundedBodySubscriber <T > {
496544
@@ -518,7 +566,7 @@ protected boolean checkSize(ByteBuffer buffer) {
518566 // The limit is within reach, so account for every line exactly.
519567 for (int i = position ; i < limit ; i ++) {
520568 byte b = buffer .get (i );
521- if (b == '\n' ) {
569+ if (b == '\n' || b == '\r' ) {
522570 this .bytesSinceLineTerminator = 0 ;
523571 }
524572 else if (++this .bytesSinceLineTerminator > this .maxSize ) {
@@ -529,12 +577,13 @@ else if (++this.bytesSinceLineTerminator > this.maxSize) {
529577 }
530578
531579 /**
532- * Returns the number of bytes after the last LF in the buffer, or the whole span
533- * added to the running count when the buffer holds no LF .
580+ * Returns the number of bytes after the last line terminator in the buffer, or
581+ * the whole span added to the running count when the buffer holds none .
534582 */
535583 private long lengthOfTrailingRun (ByteBuffer buffer , int position , int limit ) {
536584 for (int i = limit - 1 ; i >= position ; i --) {
537- if (buffer .get (i ) == '\n' ) {
585+ byte b = buffer .get (i );
586+ if (b == '\n' || b == '\r' ) {
538587 return limit - 1 - i ;
539588 }
540589 }
0 commit comments