@@ -91,68 +91,157 @@ private static boolean isChunkedBodyComplete(String body) {
9191 }
9292
9393 public static String readRawRequest (ByteBuffer initial , InputStream in ) throws IOException {
94+ // BufferedInputStream을 한 번만 생성하여 재사용 (데이터 손실 방지)
95+ BufferedInputStream bin = new BufferedInputStream (in );
96+
97+ // Phase 1: 헤더만 먼저 읽기 (메서드 분리)
98+ String headerPart = readHeadersFromStream (initial , bin );
99+
100+ // Phase 2: 조기 리턴 - 헤더가 불완전한 경우
101+ int headerEnd = headerPart .indexOf ("\r \n \r \n " );
102+ if (headerEnd < 0 ) {
103+ return headerPart ; // 잘못된 요청
104+ }
105+
106+ String headers = headerPart .substring (0 , headerEnd );
107+ String bodyStart = headerPart .substring (headerEnd + 4 );
108+
109+ // Phase 2: 조기 리턴 - Content-Length 케이스 (대부분의 HTTP 요청, 80%+)
110+ int contentLength = parseContentLength (headers );
111+ if (contentLength > 0 ) {
112+ String body = readBodyWithContentLength (bin , contentLength , bodyStart );
113+ return headers + "\r \n \r \n " + body ;
114+ }
115+
116+ // Content-Length: 0인 경우 (바디 없는 POST 등)
117+ if (contentLength == 0 ) {
118+ return headers + "\r \n \r \n " + bodyStart ;
119+ }
120+
121+ // Phase 2: 조기 리턴 - Chunked 케이스 (10% 미만)
122+ if (isChunked (headers )) {
123+ String chunkedBody = readChunkedBody (bin );
124+ return headers + "\r \n \r \n " + bodyStart + chunkedBody ;
125+ }
126+
127+ // Phase 2: 조기 리턴 - 바디가 없는 케이스 (GET 등)
128+ return headers + "\r \n \r \n " + bodyStart ;
129+ }
130+
131+ private static String readHeadersFromStream (ByteBuffer initial , BufferedInputStream bin ) throws IOException {
94132 StringBuilder sb = new StringBuilder ();
95133
96- // 1) initial buffer
134+ // 1) initial buffer 처리
97135 if (initial != null && initial .hasRemaining ()) {
98136 byte [] arr = new byte [initial .remaining ()];
99137 initial .get (arr );
100138 sb .append (new String (arr , StandardCharsets .UTF_8 ));
101139 }
102140
103- // 2) 헤더 끝까지 읽기
104- BufferedInputStream bin = new BufferedInputStream (in );
141+ // 2) 헤더 끝(\r\n\r\n)까지 읽기
105142 while (!sb .toString ().contains ("\r \n \r \n " )) {
106143 int ch = bin .read ();
107144 if (ch == -1 ) break ; // 연결 끊김
108145 sb .append ((char ) ch );
109146 }
110147
111- // 파싱해서 Content-Length or chunked 확인
112- String headerPart = sb .toString ();
113- int headerEnd = headerPart .indexOf ("\r \n \r \n " );
114- if (headerEnd < 0 ) return headerPart ; // 잘못된 요청
148+ return sb .toString ();
149+ }
115150
116- String headers = headerPart .substring (0 , headerEnd );
117- String bodyStart = headerPart .substring (headerEnd + 4 );
151+ private static String readBodyWithContentLength (BufferedInputStream bin , int contentLength , String bodyStart ) throws IOException {
152+ int alreadyRead = bodyStart .getBytes (StandardCharsets .UTF_8 ).length ;
153+ int remaining = contentLength - alreadyRead ;
118154
119- int contentLength = parseContentLength (headers ); // 없으면 -1
120- boolean chunked = isChunked (headers );
121-
122- if (chunked ) {
123- // TODO: chunked 디코딩
124- bodyStart += readChunkedBody (bin );
125- } else if (contentLength > -1 ) {
126- int alreadyRead = bodyStart .getBytes (StandardCharsets .UTF_8 ).length ;
127- int remaining = contentLength - alreadyRead ;
128- if (remaining > 0 ) {
129- byte [] bodyBytes = bin .readNBytes (remaining );
130- bodyStart += new String (bodyBytes , StandardCharsets .UTF_8 );
131- }
155+ if (remaining <= 0 ) {
156+ return bodyStart ;
132157 }
133158
134- return headers + "\r \n \r \n " + bodyStart ;
159+ byte [] bodyBytes = bin .readNBytes (remaining );
160+ return bodyStart + new String (bodyBytes , StandardCharsets .UTF_8 );
135161 }
136162
137163 private static int parseContentLength (String headers ) {
138- for (String line : headers .split ("\r \n " )) {
139- if (line .toLowerCase ().startsWith ("content-length:" )) {
140- return Integer .parseInt (line .split (":" )[1 ].trim ());
164+ int pos = 0 ;
165+ int headersLength = headers .length ();
166+
167+ while (pos < headersLength ) {
168+ int lineEnd = headers .indexOf ("\r \n " , pos );
169+ if (lineEnd < 0 ) {
170+ lineEnd = headersLength ; // 마지막 줄
141171 }
172+
173+ // "content-length:" 대소문자 무시 비교 (15자)
174+ if (regionMatchesIgnoreCase (headers , pos , "content-length:" , 15 )) {
175+ int colonIdx = headers .indexOf (':' , pos );
176+ if (colonIdx < 0 || colonIdx >= lineEnd ) {
177+ pos = lineEnd + 2 ;
178+ continue ;
179+ }
180+
181+ // 콜론 다음부터 값 시작 (공백 제거)
182+ int valueStart = colonIdx + 1 ;
183+ while (valueStart < lineEnd && headers .charAt (valueStart ) == ' ' ) {
184+ valueStart ++;
185+ }
186+
187+ // 값 끝 (공백 제거)
188+ int valueEnd = lineEnd ;
189+ while (valueEnd > valueStart && headers .charAt (valueEnd - 1 ) == ' ' ) {
190+ valueEnd --;
191+ }
192+
193+ try {
194+ return Integer .parseInt (headers .substring (valueStart , valueEnd ));
195+ } catch (NumberFormatException e ) {
196+ return -1 ;
197+ }
198+ }
199+
200+ pos = lineEnd + 2 ; // \r\n 스킵
142201 }
143202 return -1 ;
144203 }
145204
146205 private static boolean isChunked (String headers ) {
147- for (String line : headers .split ("\r \n " )) {
148- if (line .toLowerCase ().startsWith ("transfer-encoding:" )
149- && line .toLowerCase ().contains ("chunked" )) {
150- return true ;
206+ int pos = 0 ;
207+ int headersLength = headers .length ();
208+
209+ while (pos < headersLength ) {
210+ int lineEnd = headers .indexOf ("\r \n " , pos );
211+ if (lineEnd < 0 ) {
212+ lineEnd = headersLength ; // 마지막 줄
213+ }
214+
215+ // "transfer-encoding:" 대소문자 무시 비교 (18자)
216+ if (regionMatchesIgnoreCase (headers , pos , "transfer-encoding:" , 18 )) {
217+ // 해당 줄에서 "chunked" 찾기 (대소문자 무시)
218+ for (int i = pos + 18 ; i <= lineEnd - 7 ; i ++) {
219+ if (regionMatchesIgnoreCase (headers , i , "chunked" , 7 )) {
220+ return true ;
221+ }
222+ }
151223 }
224+
225+ pos = lineEnd + 2 ; // \r\n 스킵
152226 }
153227 return false ;
154228 }
155229
230+ private static boolean regionMatchesIgnoreCase (String str , int offset , String target , int length ) {
231+ if (offset + length > str .length () || length != target .length ()) {
232+ return false ;
233+ }
234+
235+ for (int i = 0 ; i < length ; i ++) {
236+ char c1 = str .charAt (offset + i );
237+ char c2 = target .charAt (i );
238+ if (c1 != c2 && Character .toLowerCase (c1 ) != Character .toLowerCase (c2 )) {
239+ return false ;
240+ }
241+ }
242+ return true ;
243+ }
244+
156245 private static String readChunkedBody (InputStream in ) throws IOException {
157246 BufferedReader r = new BufferedReader (new InputStreamReader (in , StandardCharsets .UTF_8 ));
158247 StringBuilder body = new StringBuilder ();
0 commit comments