11/*
2- * Copyright (c) 2024 Oracle and/or its affiliates.
2+ * Copyright (c) 2024, 2026 Oracle and/or its affiliates.
33 *
44 * Licensed under the Apache License, Version 2.0 (the "License");
55 * you may not use this file except in compliance with the License.
1616
1717package io .helidon .webserver .http2 ;
1818
19+ import java .nio .charset .StandardCharsets ;
20+ import java .util .ArrayList ;
1921import java .util .Base64 ;
22+ import java .util .List ;
23+ import java .util .concurrent .atomic .AtomicBoolean ;
2024
2125import io .helidon .common .buffers .BufferData ;
26+ import io .helidon .common .buffers .DataReader ;
2227import io .helidon .common .buffers .DataWriter ;
28+ import io .helidon .http .http2 .Http2Exception ;
29+ import io .helidon .http .http2 .Http2FrameData ;
30+ import io .helidon .http .http2 .Http2FrameHeader ;
31+ import io .helidon .http .http2 .Http2FrameType ;
32+ import io .helidon .http .http2 .Http2GoAway ;
2333import io .helidon .http .HeaderValues ;
2434import io .helidon .http .HttpPrologue ;
2535import io .helidon .http .Method ;
2636import io .helidon .http .WritableHeaders ;
2737import io .helidon .http .http2 .Http2Flag ;
38+ import io .helidon .http .http2 .Http2ErrorCode ;
2839import io .helidon .http .http2 .Http2Settings ;
40+ import io .helidon .http .http2 .Http2Util ;
2941import io .helidon .webserver .ConnectionContext ;
3042import io .helidon .webserver .ListenerContext ;
3143import io .helidon .webserver .Router ;
3951import static io .helidon .http .http2 .Http2Setting .MAX_FRAME_SIZE ;
4052import static io .helidon .http .http2 .Http2Setting .MAX_HEADER_LIST_SIZE ;
4153import static org .hamcrest .MatcherAssert .assertThat ;
54+ import static org .hamcrest .Matchers .greaterThanOrEqualTo ;
4255import static org .hamcrest .Matchers .is ;
56+ import static org .hamcrest .Matchers .notNullValue ;
57+ import static org .junit .jupiter .api .Assertions .assertThrows ;
58+ import static org .mockito .ArgumentMatchers .any ;
59+ import static org .mockito .Mockito .doAnswer ;
4360import static org .mockito .Mockito .mock ;
4461import static org .mockito .Mockito .when ;
4562
@@ -62,6 +79,7 @@ public UpgradeSettingsTest() {
6279 when (ctx .router ()).thenReturn (Router .empty ());
6380 when (ctx .listenerContext ()).thenReturn (mock (ListenerContext .class ));
6481 when (ctx .dataWriter ()).thenReturn (dataWriter );
82+ when (ctx .dataReader ()).thenReturn (mock (DataReader .class ));
6583 }
6684
6785 @ Test
@@ -91,6 +109,65 @@ void urlEncodedSettings() {
91109 assertThat (s .presentValue (MAX_HEADER_LIST_SIZE ).orElseThrow (), is (256L ));
92110 }
93111
112+ @ Test
113+ void invalidMaxFrameSizeDoesNotReplaceLastValidSettings () {
114+ Http2Connection connection = new Http2Connection (ctx , Http2Config .create (), List .of ());
115+
116+ Http2Settings validSettings = Http2Settings .builder ()
117+ .add (MAX_FRAME_SIZE , 16384L )
118+ .build ();
119+ connection .clientSettings (validSettings );
120+
121+ Http2Settings invalidSettings = Http2Settings .builder ()
122+ .add (MAX_FRAME_SIZE , 0L )
123+ .build ();
124+
125+ Http2Exception exception = assertThrows (Http2Exception .class ,
126+ () -> connection .clientSettings (invalidSettings ));
127+
128+ assertThat (exception .code (), is (io .helidon .http .http2 .Http2ErrorCode .PROTOCOL ));
129+ assertThat (connection .clientSettings ().value (MAX_FRAME_SIZE ), is (16384L ));
130+ }
131+
132+ @ Test
133+ void invalidMaxFrameSizeHandledWithProtocolGoAway () throws InterruptedException {
134+ List <BufferData > writtenFrames = new ArrayList <>();
135+ DataWriter dataWriter = mock (DataWriter .class );
136+ doAnswer (invocation -> {
137+ BufferData data = invocation .getArgument (0 );
138+ writtenFrames .add (data .copy ());
139+ return null ;
140+ }).when (dataWriter ).writeNow (any (BufferData .class ));
141+
142+ ConnectionContext connectionContext = mock (ConnectionContext .class );
143+ when (connectionContext .router ()).thenReturn (Router .empty ());
144+ when (connectionContext .listenerContext ()).thenReturn (mock (ListenerContext .class ));
145+ when (connectionContext .dataWriter ()).thenReturn (dataWriter );
146+ when (connectionContext .dataReader ()).thenReturn (invalidMaxFrameSizeReader ());
147+
148+ Http2Connection connection = new Http2Connection (connectionContext ,
149+ Http2Config .builder ().sendErrorDetails (true ).build (),
150+ List .of ());
151+ connection .expectPreface ();
152+ connection .handle (mock (io .helidon .common .concurrency .limits .Limit .class ));
153+
154+ assertThat (writtenFrames .size (), greaterThanOrEqualTo (2 ));
155+
156+ BufferData goAwayData = writtenFrames .get (writtenFrames .size () - 1 );
157+ byte [] headerBytes = new byte [Http2FrameHeader .LENGTH ];
158+ goAwayData .read (headerBytes );
159+ Http2FrameHeader frameHeader = Http2FrameHeader .create (BufferData .create (headerBytes ));
160+ assertThat (frameHeader .type (), is (Http2FrameType .GO_AWAY ));
161+
162+ byte [] payloadBytes = new byte [frameHeader .length ()];
163+ goAwayData .read (payloadBytes );
164+ Http2GoAway goAway = Http2GoAway .create (BufferData .create (payloadBytes ));
165+ assertThat (goAway , notNullValue ());
166+ assertThat (goAway .errorCode (), is (Http2ErrorCode .PROTOCOL ));
167+ assertThat (new String (payloadBytes , 8 , payloadBytes .length - 8 , StandardCharsets .UTF_8 ),
168+ is ("Frame size must be between 2^14 and 2^24-1, but is: 0" ));
169+ }
170+
94171 Http2Settings upgrade (String http2Settings ) {
95172 WritableHeaders <?> headers = WritableHeaders .create ().add (HeaderValues .create ("HTTP2-Settings" , http2Settings ));
96173 Http2Upgrader http2Upgrader = Http2Upgrader .create (Http2Config .create ());
@@ -105,4 +182,16 @@ byte[] settingsToBytes(Http2Settings settings) {
105182 settingsFrameData .read (b );
106183 return b ;
107184 }
185+
186+ private static DataReader invalidMaxFrameSizeReader () {
187+ Http2FrameData frameData = Http2Settings .builder ()
188+ .add (MAX_FRAME_SIZE , 0L )
189+ .build ()
190+ .toFrameData (null , 0 , Http2Flag .SettingsFlags .create (0 ));
191+ BufferData input = BufferData .create (Http2Util .prefaceData (),
192+ BufferData .create (frameData .header ().write (), frameData .data ()));
193+ byte [] bytes = input .readBytes ();
194+ AtomicBoolean delivered = new AtomicBoolean ();
195+ return DataReader .create (() -> delivered .compareAndSet (false , true ) ? bytes : null );
196+ }
108197}
0 commit comments