77import java .net .http .HttpResponse ;
88import java .net .http .HttpResponse .BodySubscriber ;
99import java .net .http .HttpResponse .ResponseInfo ;
10+ import java .nio .ByteBuffer ;
11+ import java .nio .charset .StandardCharsets ;
12+ import java .util .List ;
1013import java .util .concurrent .atomic .AtomicReference ;
1114import java .util .regex .Pattern ;
1215
@@ -56,7 +59,7 @@ record AggregateResponseEvent(ResponseInfo responseInfo, String data) implements
5659
5760 static BodySubscriber <Void > sseToBodySubscriber (ResponseInfo responseInfo , FluxSink <ResponseEvent > sink ) {
5861 return HttpResponse .BodySubscribers
59- .fromLineSubscriber (FlowAdapters .toFlowSubscriber (new SseLineSubscriber (responseInfo , sink )));
62+ .fromSubscriber (FlowAdapters .toFlowSubscriber (new SseByteSubscriber (responseInfo , sink )));
6063 }
6164
6265 static BodySubscriber <Void > aggregateBodySubscriber (ResponseInfo responseInfo , FluxSink <ResponseEvent > sink ) {
@@ -69,56 +72,33 @@ static BodySubscriber<Void> bodilessBodySubscriber(ResponseInfo responseInfo, Fl
6972 .fromLineSubscriber (FlowAdapters .toFlowSubscriber (new BodilessResponseLineSubscriber (responseInfo , sink )));
7073 }
7174
72- static class SseLineSubscriber extends BaseSubscriber <String > {
75+ static class SseByteSubscriber extends BaseSubscriber <List < ByteBuffer > > {
7376
74- /**
75- * Pattern to extract data content from SSE "data:" lines.
76- */
7777 private static final Pattern EVENT_DATA_PATTERN = Pattern .compile ("^data:(.+)$" , Pattern .MULTILINE );
7878
79- /**
80- * Pattern to extract event ID from SSE "id:" lines.
81- */
8279 private static final Pattern EVENT_ID_PATTERN = Pattern .compile ("^id:(.+)$" , Pattern .MULTILINE );
8380
84- /**
85- * Pattern to extract event type from SSE "event:" lines.
86- */
8781 private static final Pattern EVENT_TYPE_PATTERN = Pattern .compile ("^event:(.+)$" , Pattern .MULTILINE );
8882
89- /**
90- * The sink for emitting parsed response events.
91- */
9283 private final FluxSink <ResponseEvent > sink ;
9384
94- /**
95- * StringBuilder for accumulating multi-line event data.
96- */
9785 private final StringBuilder eventBuilder ;
9886
99- /**
100- * Current event's ID, if specified.
101- */
10287 private final AtomicReference <String > currentEventId ;
10388
104- /**
105- * Current event's type, if specified.
106- */
10789 private final AtomicReference <String > currentEventType ;
10890
109- /**
110- * The response information from the HTTP response. Send with each event to
111- * provide context.
112- */
113- private ResponseInfo responseInfo ;
91+ private final ResponseInfo responseInfo ;
11492
115- /**
116- * Creates a new LineSubscriber that will emit parsed SSE events to the provided
117- * sink.
118- * @param sink the {@link FluxSink} to emit parsed {@link ResponseEvent} objects
119- * to
120- */
121- public SseLineSubscriber (ResponseInfo responseInfo , FluxSink <ResponseEvent > sink ) {
93+ private final SseByteBuffer buffer = new SseByteBuffer ();
94+
95+ private volatile boolean hasRequestedDemand = false ;
96+
97+ private int scanIndex = 0 ;
98+
99+ private int start = 0 ;
100+
101+ public SseByteSubscriber (ResponseInfo responseInfo , FluxSink <ResponseEvent > sink ) {
122102 this .sink = sink ;
123103 this .eventBuilder = new StringBuilder ();
124104 this .currentEventId = new AtomicReference <>();
@@ -128,21 +108,71 @@ public SseLineSubscriber(ResponseInfo responseInfo, FluxSink<ResponseEvent> sink
128108
129109 @ Override
130110 protected void hookOnSubscribe (Subscription subscription ) {
131-
132111 sink .onRequest (n -> {
133- subscription .request (n );
112+ if (!hasRequestedDemand ) {
113+ subscription .request (Long .MAX_VALUE );
114+ }
115+ hasRequestedDemand = true ;
134116 });
135117
136- // Register disposal callback to cancel subscription when Flux is disposed
137118 sink .onDispose (() -> {
138119 subscription .cancel ();
139120 });
140121 }
141122
142123 @ Override
143- protected void hookOnNext (String line ) {
124+ protected void hookOnNext (List <ByteBuffer > buffers ) {
125+ for (ByteBuffer b : buffers ) {
126+ int remaining = b .remaining ();
127+ if (remaining > 0 ) {
128+ byte [] bytes = new byte [remaining ];
129+ b .get (bytes );
130+ buffer .append (bytes , 0 , remaining );
131+ }
132+ }
133+ parseBuffer ();
134+ }
135+
136+ private void parseBuffer () {
137+ byte [] buf = buffer .getBuf ();
138+ int count = buffer .getCount ();
139+
140+ while (scanIndex < count ) {
141+ byte b = buf [scanIndex ];
142+ if (b == '\n' ) {
143+ int lineEnd = scanIndex ;
144+ int terminatorLen = 1 ;
145+ processLine (buf , start , lineEnd );
146+ start = lineEnd + terminatorLen ;
147+ scanIndex = start ;
148+ }
149+ else if (b == '\r' ) {
150+ if (scanIndex + 1 < count ) {
151+ int lineEnd = scanIndex ;
152+ int terminatorLen = (buf [scanIndex + 1 ] == '\n' ) ? 2 : 1 ;
153+ processLine (buf , start , lineEnd );
154+ start = lineEnd + terminatorLen ;
155+ scanIndex = start ;
156+ }
157+ else {
158+ break ;
159+ }
160+ }
161+ else {
162+ scanIndex ++;
163+ }
164+ }
165+
166+ if (start > 0 ) {
167+ buffer .shift (start );
168+ scanIndex -= start ;
169+ start = 0 ;
170+ }
171+ }
172+
173+ private void processLine (byte [] buf , int start , int end ) {
174+ String line = new String (buf , start , end - start , StandardCharsets .UTF_8 );
144175 if (line .isEmpty ()) {
145- // Empty line means end of event
146176 if (this .eventBuilder .length () > 0 ) {
147177 String eventData = this .eventBuilder .toString ();
148178 SseEvent sseEvent = new SseEvent (currentEventId .get (), currentEventType .get (), eventData .trim ());
@@ -157,39 +187,47 @@ protected void hookOnNext(String line) {
157187 if (matcher .find ()) {
158188 this .eventBuilder .append (matcher .group (1 ).trim ()).append ("\n " );
159189 }
160- upstream ().request (1 );
161190 }
162191 else if (line .startsWith ("id:" )) {
163192 var matcher = EVENT_ID_PATTERN .matcher (line );
164193 if (matcher .find ()) {
165194 this .currentEventId .set (matcher .group (1 ).trim ());
166195 }
167- upstream ().request (1 );
168196 }
169197 else if (line .startsWith ("event:" )) {
170198 var matcher = EVENT_TYPE_PATTERN .matcher (line );
171199 if (matcher .find ()) {
172200 this .currentEventType .set (matcher .group (1 ).trim ());
173201 }
174- upstream ().request (1 );
175202 }
176203 else if (line .startsWith (":" )) {
177- // Ignore comment lines starting with ":"
178- // This is a no-op, just to skip comments
179204 logger .debug ("Ignoring comment line: {}" , line );
180- upstream ().request (1 );
181205 }
182206 else {
183- // If the response is not successful, emit an error
184207 this .sink .error (new McpTransportException (
185208 "Invalid SSE response. Status code: " + this .responseInfo .statusCode () + " Line: " + line ));
186-
187209 }
188210 }
189211 }
190212
191213 @ Override
192214 protected void hookOnComplete () {
215+ byte [] buf = buffer .getBuf ();
216+ int count = buffer .getCount ();
217+
218+ // If we broke out of the loop because of a trailing '\r' at the end of the
219+ // stream,
220+ // treat it as a bare '\r' line terminator now.
221+ if (scanIndex < count && buf [scanIndex ] == '\r' ) {
222+ int lineEnd = scanIndex ;
223+ int terminatorLen = 1 ;
224+ processLine (buf , start , lineEnd );
225+ start = lineEnd + terminatorLen ;
226+ }
227+
228+ if (start < count ) {
229+ processLine (buf , start , count );
230+ }
193231 if (this .eventBuilder .length () > 0 ) {
194232 String eventData = this .eventBuilder .toString ();
195233 SseEvent sseEvent = new SseEvent (currentEventId .get (), currentEventType .get (), eventData .trim ());
@@ -205,6 +243,52 @@ protected void hookOnError(Throwable throwable) {
205243
206244 }
207245
246+ private static class SseByteBuffer {
247+
248+ private byte [] buf = new byte [4096 ];
249+
250+ private int count = 0 ;
251+
252+ public void append (byte [] b , int off , int len ) {
253+ ensureCapacity (count + len );
254+ System .arraycopy (b , off , buf , count , len );
255+ count += len ;
256+ }
257+
258+ private void ensureCapacity (int minCapacity ) {
259+ if (minCapacity - buf .length > 0 ) {
260+ int newCapacity = buf .length * 2 ;
261+ if (newCapacity - minCapacity < 0 ) {
262+ newCapacity = minCapacity ;
263+ }
264+ byte [] newBuf = new byte [newCapacity ];
265+ System .arraycopy (buf , 0 , newBuf , 0 , count );
266+ buf = newBuf ;
267+ }
268+ }
269+
270+ public byte [] getBuf () {
271+ return buf ;
272+ }
273+
274+ public int getCount () {
275+ return count ;
276+ }
277+
278+ public void shift (int bytesToShift ) {
279+ if (bytesToShift <= 0 ) {
280+ return ;
281+ }
282+ if (bytesToShift >= count ) {
283+ count = 0 ;
284+ return ;
285+ }
286+ System .arraycopy (buf , bytesToShift , buf , 0 , count - bytesToShift );
287+ count -= bytesToShift ;
288+ }
289+
290+ }
291+
208292 static class AggregateSubscriber extends BaseSubscriber <String > {
209293
210294 /**
0 commit comments