RequestParser.java

/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2025 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package io.undertow.server.protocol.http;

import static io.undertow.server.protocol.http.RequestState.ABSOLUTE_FORM;
import static io.undertow.server.protocol.http.RequestState.ASTERISK_FORM;
import static io.undertow.server.protocol.http.RequestState.AUTHORITY_FORM;
import static io.undertow.server.protocol.http.RequestState.DNS;
import static io.undertow.server.protocol.http.RequestState.FIELD_NAME;
import static io.undertow.server.protocol.http.RequestState.FIELD_VALUE;
import static io.undertow.server.protocol.http.RequestState.HOST;
import static io.undertow.server.protocol.http.RequestState.IPV4;
import static io.undertow.server.protocol.http.RequestState.IPV6;
import static io.undertow.server.protocol.http.RequestState.MESSAGE_BODY;
import static io.undertow.server.protocol.http.RequestState.METHOD;
import static io.undertow.server.protocol.http.RequestState.ORIGIN_FORM;
import static io.undertow.server.protocol.http.RequestState.PATH_PARAMS;
import static io.undertow.server.protocol.http.RequestState.PATH_SEGMENTS;
import static io.undertow.server.protocol.http.RequestState.PORT;
import static io.undertow.server.protocol.http.RequestState.QUERY_PARAMS;
import static io.undertow.server.protocol.http.RequestState.REQUEST_TARGET;
import static io.undertow.server.protocol.http.RequestState.SCHEME;
import static io.undertow.server.protocol.http.RequestState.UNKNOWN;
import static io.undertow.server.protocol.http.RequestState.UNSPECIFIED;
import static io.undertow.server.protocol.http.RequestState.VERSION;
import static io.undertow.util.Methods.CONNECT;
import static io.undertow.util.Methods.OPTIONS;
import static io.undertow.util.Methods.PRI;
import static io.undertow.util.ParserUtils.AMPERSAND;
import static io.undertow.util.ParserUtils.ASTERISK;
import static io.undertow.util.ParserUtils.COLON;
import static io.undertow.util.ParserUtils.COMMA;
import static io.undertow.util.ParserUtils.CARRIAGE_RETURN;
import static io.undertow.util.ParserUtils.DOT;
import static io.undertow.util.ParserUtils.EQUALS;
import static io.undertow.util.ParserUtils.LEFT_SQUARE_BRACKET;
import static io.undertow.util.ParserUtils.LINE_FEED;
import static io.undertow.util.ParserUtils.PERCENT;
import static io.undertow.util.ParserUtils.PLUS;
import static io.undertow.util.ParserUtils.QUESTION;
import static io.undertow.util.ParserUtils.RIGHT_SQUARE_BRACKET;
import static io.undertow.util.ParserUtils.SEMICOLON;
import static io.undertow.util.ParserUtils.SLASH;
import static io.undertow.util.ParserUtils.SPACE;
import static io.undertow.util.ParserUtils.getMaximumRequestMethodLength;
import static io.undertow.util.ParserUtils.getProtocolLength;
import static io.undertow.util.ParserUtils.isAlphaChar;
import static io.undertow.util.ParserUtils.isDigitChar;
import static io.undertow.util.ParserUtils.isDNSNameChar;
import static io.undertow.util.ParserUtils.isHexDigitChar;
import static io.undertow.util.ParserUtils.isIPv4AddressChar;
import static io.undertow.util.ParserUtils.isIPv6AddressChar;
import static io.undertow.util.ParserUtils.isObsoleteChar;
import static io.undertow.util.ParserUtils.isPathSegmentChar;
import static io.undertow.util.ParserUtils.isProtocolChar;
import static io.undertow.util.ParserUtils.isRequestTargetChar;
import static io.undertow.util.ParserUtils.isSchemeChar;
import static io.undertow.util.ParserUtils.isSpaceOrTabChar;
import static io.undertow.util.ParserUtils.isTokenChar;
import static io.undertow.util.ParserUtils.isVisibleAsciiChar;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

import io.undertow.UndertowLogger;
import io.undertow.UndertowMessages;
import io.undertow.UndertowOptions;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import io.undertow.util.Methods;
import io.undertow.util.Protocols;
import io.undertow.util.URLUtils;
import io.undertow.util.BadRequestException;

import org.xnio.OptionMap;

/**
 * @author <a href="mailto:opalka.richard@gmail.com">Richard Opalka</a>
 */
final class RequestParser {
    private final int maxParameters;
    private final int maxHeaders;
    private final boolean slashDecodingFlag;
    private final boolean decode;
    private final String charset;
    private final boolean allowUnescapedCharactersInUrl;

    private RequestParser(final OptionMap options) {
        maxParameters = options.get(UndertowOptions.MAX_PARAMETERS, UndertowOptions.DEFAULT_MAX_PARAMETERS);
        maxHeaders = options.get(UndertowOptions.MAX_HEADERS, UndertowOptions.DEFAULT_MAX_HEADERS);
        slashDecodingFlag = URLUtils.getSlashDecodingFlag(options);
        decode = options.get(UndertowOptions.DECODE_URL, true);
        charset = options.get(UndertowOptions.URL_CHARSET, StandardCharsets.UTF_8.name());
        allowUnescapedCharactersInUrl = options.get(UndertowOptions.ALLOW_UNESCAPED_CHARACTERS_IN_URL, false);
    }

    static RequestParser instance(final OptionMap options) {
        return new RequestParser(options);
    }

    void handle(final ByteBuffer buffer, final RequestState state, final HttpServerExchange builder) throws BadRequestException {
        parseMethod(buffer, state, builder);
        while (buffer.hasRemaining() && state.state < VERSION) {
            parseRequestTarget(buffer, state, builder);
        }
        parseVersion(buffer, state, builder);
        while (buffer.hasRemaining() && !state.isComplete()) {
            parseFieldName(buffer, state, builder);
            parseFieldValue(buffer, state, builder);
        }
    }

    private void parseMethod(final ByteBuffer buffer, final RequestState state, final HttpServerExchange builder) throws BadRequestException {
        if (state.state != METHOD || !buffer.hasRemaining()) return;

        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (isTokenChar(nextByte) && sb.length() <= getMaximumRequestMethodLength()) {
                // method data goes on
                sb.append(nextChar);
            } else if (nextByte == SPACE && sb.length() != 0) {
                // method read complete
                final String requestMethod = sb.toString();
                builder.setRequestMethod(Methods.fromString(requestMethod));
                // prepare for next parsing phase
                state.setNext(REQUEST_TARGET);
                return;
            } else throw new BadRequestException();
        }
    }

    private void parseRequestTarget(final ByteBuffer buffer, final RequestState state, final HttpServerExchange builder) throws BadRequestException {
        if (state.state != REQUEST_TARGET || !buffer.hasRemaining()) return;

        // Detect request target type
        if (state.targetType == UNKNOWN) {
            final byte nextByte = buffer.get();

            if (nextByte == SLASH) {
                state.targetType = ORIGIN_FORM;
                state.substate = PATH_SEGMENTS;
            } else if (PRI.equals(builder.getRequestMethod()) || OPTIONS.equals(builder.getRequestMethod()) && nextByte == ASTERISK) {
                state.targetType = ASTERISK_FORM;
                state.substate = PATH_SEGMENTS;
            } else if (CONNECT.equals(builder.getRequestMethod())) {
                state.targetType = AUTHORITY_FORM;
                state.substate = HOST;
            } else if (isAlphaChar(nextByte)) {
                state.targetType = ABSOLUTE_FORM;
                state.substate = SCHEME;
            } else throw new BadRequestException();

            buffer.position(buffer.position() - 1);
        }

        // Process request target
        while (buffer.hasRemaining() && state.state != VERSION) {
            if (state.targetType == ORIGIN_FORM) {
                parsePath(buffer, state, builder);
            } else if (state.targetType == ABSOLUTE_FORM) {
                parseScheme(buffer, state);
                parseHost(buffer, state);
                parsePort(buffer, state);
                parsePath(buffer, state, builder);
            } else if (state.targetType == AUTHORITY_FORM) {
                parseHost(buffer, state);
                parsePort(buffer, state);
                parsePath(buffer, state, builder);
            } else if (state.targetType == ASTERISK_FORM) {
                parsePath(buffer, state, builder);
            } else throw new IllegalStateException();
        }
    }

    private void parseVersion(final ByteBuffer buffer, final RequestState state, final HttpServerExchange builder) throws BadRequestException {
        if (state.state != VERSION || !buffer.hasRemaining()) return;

        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (state.previousByte == 0 && isProtocolChar(nextByte, sb.length())) {
                // http version data goes on
                sb.append(nextChar);
            } else if (state.previousByte == 0 && nextByte == CARRIAGE_RETURN && sb.length() == getProtocolLength()) {
                // http version read complete - first separator
                state.previousByte = nextByte;
            } else if (state.previousByte == CARRIAGE_RETURN && nextByte == LINE_FEED) {
                // http version read complete - second separator
                final String version = sb.toString();
                if (Protocols.HTTP_0_9.toString().equals(version)) builder.setProtocol(Protocols.HTTP_0_9);
                else if (Protocols.HTTP_1_0.toString().equals(version)) builder.setProtocol(Protocols.HTTP_1_0);
                else if (Protocols.HTTP_1_1.toString().equals(version)) builder.setProtocol(Protocols.HTTP_1_1);
                else if (Protocols.HTTP_2_0.toString().equals(version)) builder.setProtocol(Protocols.HTTP_2_0);
                else {
                    if (UndertowLogger.REQUEST_LOGGER.isTraceEnabled()) {
                        UndertowLogger.REQUEST_LOGGER.tracef("Unsupported HTTP version %s; falling back to HTTP 1.1", version);
                    }
                    builder.setProtocol(Protocols.HTTP_1_1);
                }
                // prepare for next parsing phase
                state.setNext(FIELD_NAME);
                return;
            } else throw new BadRequestException();
        }
    }

    private void parseFieldName(final ByteBuffer buffer, final RequestState state, final HttpServerExchange builder) throws BadRequestException {
        if (state.state != FIELD_NAME || !buffer.hasRemaining()) return;

        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (state.previousByte == 0 && isTokenChar(nextByte)) {
                // field name data goes on
                sb.append(nextChar);
            } else if (state.previousByte == 0 && nextByte == COLON && sb.length() != 0) {
                // field name read complete
                final String headerName = sb.toString();
                // prepare for next parsing phase
                state.setNext(FIELD_VALUE);
                state.headerName = Headers.fromCache(headerName);
                if (state.headerName == null) state.headerName = new HttpString(headerName);
                return;
            } else if (state.previousByte == 0 && nextByte == CARRIAGE_RETURN && sb.length() == 0) {
                // special case - no headers available
                state.previousByte = nextByte;
            } else if (state.previousByte == CARRIAGE_RETURN && nextByte == LINE_FEED && sb.length() == 0) {
                // prepare for next parsing phase
                state.setNext(MESSAGE_BODY);
                return;
            } else throw new BadRequestException();
        }
    }

    private void parseFieldValue(final ByteBuffer buffer, final RequestState state, final HttpServerExchange builder) throws BadRequestException {
        if (state.state != FIELD_VALUE || !buffer.hasRemaining()) return;

        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (state.previousByte == 0 && sb.length() == 0 && isSpaceOrTabChar(nextByte)) {
                // heading OWS
            } else if (state.previousByte == 0 && (isVisibleAsciiChar(nextByte) || isObsoleteChar(nextByte))) {
                // field-vchar
                sb.append(nextChar);
            } else if (state.previousByte == 0 && isSpaceOrTabChar(nextByte)) {
                // field-vchar RWS or OWS or obs-fold
                state.previousByte = nextByte;
            } else if (isSpaceOrTabChar(state.previousByte) && (isVisibleAsciiChar(nextByte) || isObsoleteChar(nextByte))) {
                // field-vchar RWS field-vchar
                sb.append((char) (state.previousByte & 0xFF));
                sb.append(nextChar);
                state.previousByte = 0;
            } else if (isSpaceOrTabChar(state.previousByte) && isSpaceOrTabChar(nextByte)) {
                // field-vchar OWS or obs-fold
            } else if (isSpaceOrTabChar(state.previousByte) && nextByte == CARRIAGE_RETURN) {
                // field-vchar OWS or obs-fold
                state.previousByte = nextByte;
            } else if (state.previousByte == 0 && nextByte == CARRIAGE_RETURN) {
                // CRLF or obs-fold
                state.previousByte = nextByte;
            } else if (state.previousByte == CARRIAGE_RETURN && nextByte == LINE_FEED) {
                // CRLF or obs-fold
                state.previousByte = nextByte;
            } else if (state.previousByte == LINE_FEED && isSpaceOrTabChar(nextByte)) {
                // obs-fold
                if (sb.length() > 0) {
                    byte[] bytes = sb.toString().getBytes(StandardCharsets.US_ASCII);
                    if (!isSpaceOrTabChar(bytes[bytes.length - 1])) sb.append((char)SPACE);
                }
                state.previousByte = 0;
            } else if (state.previousByte == LINE_FEED && isTokenChar(nextByte)) {
                // CRLF header
                final String headerValue = sb.toString().trim();
                addHeader(state, builder, headerValue);
                // prepare for next parsing phase
                state.setNext(FIELD_NAME);
                sb.append(nextChar);
                return;
            } else if (state.previousByte == LINE_FEED && nextByte == CARRIAGE_RETURN) {
                // CRLF CRLF
                final String headerValue = sb.toString().trim();
                addHeader(state, builder, headerValue);
                // prepare for next parsing phase
                state.setNext(FIELD_NAME);
                state.previousByte = nextByte;
                return;
            } else throw new BadRequestException();
        }
    }

    private void parseScheme(final ByteBuffer buffer, final RequestState state) throws BadRequestException {
        if (state.substate != SCHEME || !buffer.hasRemaining()) return;

        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (state.previousByte == 0 && isSchemeChar(nextByte)) {
                // scheme data goes on
                sb.append(nextChar);
            } else if (state.previousByte == 0 && nextByte == COLON) {
                // scheme data goes on - first separator
                sb.append(nextChar);
                state.previousByte = nextByte;
            } else if (state.previousByte == COLON && nextByte == SLASH) {
                // scheme data goes on - second separator
                sb.append(nextChar);
                state.previousByte = nextByte;
            } else if (state.previousByte == SLASH && nextByte == SLASH) {
                // scheme read complete
                sb.append(nextChar);
                // prepare for next parsing phase
                state.previousByte = 0;
                state.substate = HOST;
                return;
            } else throw new BadRequestException();
        }
    }

    private void parseHost(final ByteBuffer buffer, final RequestState state) throws BadRequestException {
        if (state.substate != HOST || !buffer.hasRemaining()) return;

        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (state.addressType == UNSPECIFIED) {
                // detect address type
                if (nextByte == LEFT_SQUARE_BRACKET) {
                    state.addressType = IPV6;
                } else if (isDigitChar(nextByte)) {
                    state.addressType = IPV4;
                } else if (isPercentEncodedSequenceChar(nextByte, state)) {
                    state.addressType = DNS;
                } else if (isDNSNameChar(nextByte)) {
                    state.addressType = DNS;
                } else throw new BadRequestException();
                sb.append(nextChar);
            } else if (state.addressType == DNS) {
                if (isPercentEncodedSequenceChar(nextByte, state)) {
                    // DNS name data goes on - percent encoded sequence
                    sb.append(nextChar);
                } else if (isDNSNameChar(nextByte)) {
                    // DNS name data goes on
                    sb.append(nextChar);
                } else {
                    // DNS name read complete - prepare for next parsing phase
                    buffer.position(buffer.position() -1);
                    state.substate = PORT;
                    return;
                }
            } else if (state.addressType == IPV4) {
                if (isIPv4AddressChar(nextByte)) {
                    // IPv4 address data goes on
                    sb.append(nextChar);
                } else if (isPercentEncodedSequenceChar(nextByte, state)) {
                    // Not IPv4 address - percent encoded DNS name detected
                    sb.append(nextChar);
                    state.addressType = DNS;
                } else if (isDNSNameChar(nextByte)) {
                    // Not IPv4 address - DNS name detected
                    sb.append(nextChar);
                    state.addressType = DNS;
                } else {
                    // IPv4 address read complete - prepare for next parsing phase
                    buffer.position(buffer.position() -1);
                    state.substate = PORT;
                    return;
                }
            } else if (state.addressType == IPV6) {
                if (isIPv6AddressChar(nextByte)) {
                    // IPv6 address data goes on
                    sb.append(nextChar);
                } else if (nextByte == RIGHT_SQUARE_BRACKET) {
                    // IPv6 address read complete
                    sb.append(nextChar);
                    // prepare for next parsing phase
                    state.substate = PORT;
                    return;
                } else throw new BadRequestException();
            } else throw new IllegalStateException();
        }
    }

    private void parsePort(final ByteBuffer buffer, final RequestState state) throws BadRequestException {
        if (state.substate != PORT || !buffer.hasRemaining()) return;

        final boolean isPortOptional = state.targetType != AUTHORITY_FORM;
        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (state.previousByte == 0 && nextByte == COLON) {
                // port data read start
                sb.append(nextChar);
                state.previousByte = nextByte;
            } else if (state.previousByte == COLON && isDigitChar(nextByte)) {
                // port data goes on
                sb.append(nextChar);
                state.previousByte = nextByte;
            } else if (isDigitChar(state.previousByte) && isDigitChar(nextByte)) {
                // port data goes on
                sb.append(nextChar);
                state.previousByte = nextByte;
            } else if (isDigitChar(state.previousByte) && !isDigitChar(nextByte)) {
                // port read complete
                state.previousByte = 0;
                // prepare for next parsing phase
                buffer.position(buffer.position() -1);
                state.substate = PATH_SEGMENTS;
                state.position = sb.length();
                return;
            } else if (state.previousByte == 0 && isPortOptional) {
                // port is optional and was not provided - prepare for next parsing phase
                buffer.position(buffer.position() -1);
                state.substate = PATH_SEGMENTS;
                state.position = sb.length();
                return;
            } else throw new BadRequestException();
        }
    }

    private void parsePath(final ByteBuffer buffer, final RequestState state, final HttpServerExchange builder) throws BadRequestException {
        if (state.substate != PATH_SEGMENTS && state.substate != PATH_PARAMS && state.substate != QUERY_PARAMS || !buffer.hasRemaining()) return;

        while (buffer.hasRemaining() && state.substate < QUERY_PARAMS && state.state != VERSION) {
            parsePathSegments(buffer, state, builder);
            parsePathParameters(buffer, state, builder);
        }
        while (buffer.hasRemaining() && state.state != VERSION) {
            parseQueryParameters(buffer, state, builder);
        }
    }

    private void parsePathSegments(final ByteBuffer buffer, final RequestState state, final HttpServerExchange builder) throws BadRequestException {
        if (state.substate != PATH_SEGMENTS || !buffer.hasRemaining()) return;

        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (state.targetType == ORIGIN_FORM || state.targetType == ABSOLUTE_FORM) {
                if (isPercentEncodedSequenceChar(nextByte, state)) {
                    // percent encoded data
                    sb.append(nextChar);
                } else if (isDoubleDotSegmentCandidate(nextByte, state)) {
                    // double dot segment sequence
                    sb.append(nextChar);
                } else if (nextByte == SEMICOLON) {
                    // path parameters section beings
                    state.canonicalPath.append(sb.substring(state.position));
                    if (state.doubleDotSegment)
                        state.canonicalPath.append(nextChar); // special case, see UNDERTOW-2339
                    sb.append(nextChar);
                    state.position = sb.length();
                    // prepare for next parsing phase
                    state.previousByte = 0;
                    state.substate = PATH_PARAMS;
                    return;
                } else if (isPathSegmentChar(nextByte)) {
                    // path segment data continues
                    sb.append(nextChar);
                } else if (nextByte == QUESTION) {
                    // query parameters section begins
                    addPaths(state, builder);
                    // prepare for next parsing phase
                    state.setNext(REQUEST_TARGET, QUERY_PARAMS);
                    return;
                } else if (nextByte == SPACE) {
                    // end of request target
                    addPaths(state, builder);
                    // prepare for next parsing phase
                    state.setNext(VERSION);
                    return;
                } else if (isRequestTargetChar(nextByte)) {
                    // RFC 3986, 2.2 Reserved Characters
                    // If a reserved character is found in a URI component and no delimiting role is known for that character, then
                    // it must be interpreted as representing the data octet corresponding to that character's encoding in US-ASCII.
                    sb.append(nextChar);
                } else if (allowUnescapedCharactersInUrl) {
                    // unescaped characters - violating HTTP 1.1 specification
                    sb.append(nextChar);
                    state.urlDecodeRequired = decode;
                } else throw new BadRequestException();
            } else if (state.targetType == AUTHORITY_FORM) {
                if (state.previousByte == 0 && nextByte == SPACE) {
                    // end of request target
                    addPaths(state, builder);
                    // prepare for next parsing phase
                    state.setNext(VERSION);
                    return;
                } else throw new BadRequestException();
            } else if (state.targetType == ASTERISK_FORM) {
                if (state.previousByte == 0 && nextByte == ASTERISK) {
                    // asterisk read complete
                    sb.append(nextChar);
                    state.previousByte = nextByte;
                } else if  (state.previousByte == ASTERISK && nextByte == SPACE) {
                    // end of request target
                    addPaths(state, builder);
                    // prepare for next parsing phase
                    state.setNext(VERSION);
                    return;
                } else throw new BadRequestException();
            } else throw new IllegalStateException();
        }
    }

    private void parsePathParameters(final ByteBuffer buffer, final RequestState state, final HttpServerExchange builder) throws BadRequestException {
        if (state.substate != PATH_PARAMS || !buffer.hasRemaining()) return;

        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (isPercentEncodedSequenceChar(nextByte, state)) {
                // percent encoded data
                sb.append(nextChar);
            } else if (nextByte == PLUS) {
                // encoded space
                sb.append(nextChar);
                state.paramDecodeRequired = state.urlDecodeRequired = decode;
            } else if (nextByte == EQUALS && state.paramName == null) {
                // path parameter key read complete
                state.paramName = decode(sb.substring(state.position), state.paramDecodeRequired, state, true, true);
                sb.append(nextChar);
                state.position = sb.length();
                state.paramDecodeRequired = false;
            } else if (nextByte == COMMA) {
                // potential path parameter value read complete
                sb.append(nextChar);
            } else if (nextByte == SEMICOLON) {
                // path parameter value read complete
                addParam(state, builder);
                sb.append(nextChar);
                state.position = sb.length();
                state.paramName = null;
                state.paramDecodeRequired = false;
            } else if (isPathSegmentChar(nextByte)) {
                // path parameters data continues
                sb.append(nextChar);
            } else if (nextByte == SLASH) {
                // path segments section continues
                addParam(state, builder);
                state.previousByte = nextByte;
                state.position = sb.length();
                sb.append(nextChar);
                state.paramName = null;
                // prepare for next parsing phase
                state.substate = PATH_SEGMENTS;
                return;
            } else if (nextByte == QUESTION) {
                // query parameters section begins
                addParam(state, builder);
                state.position = sb.length();
                addPaths(state, builder);
                // prepare for next parsing phase
                state.setNext(REQUEST_TARGET, QUERY_PARAMS);
                return;
            } else if (nextByte == SPACE) {
                // end of request target
                addParam(state, builder);
                state.position = sb.length();
                addPaths(state, builder);
                // prepare for next parsing phase
                state.setNext(VERSION);
                return;
            } else if (isRequestTargetChar(nextByte)) {
                // RFC 3986, 2.2 Reserved Characters
                // If a reserved character is found in a URI component and no delimiting role is known for that character, then
                // it must be interpreted as representing the data octet corresponding to that character's encoding in US-ASCII.
                sb.append(nextChar);
            } else if (allowUnescapedCharactersInUrl) {
                // unescaped characters - violating HTTP 1.1 specification
                sb.append(nextChar);
                state.paramDecodeRequired = state.urlDecodeRequired = decode;
            } else throw new BadRequestException();
        }
    }

    private void parseQueryParameters(final ByteBuffer buffer, final RequestState state, final HttpServerExchange builder) throws BadRequestException {
        if (state.substate != QUERY_PARAMS || !buffer.hasRemaining()) return;

        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (isPercentEncodedSequenceChar(nextByte, state)) {
                // percent encoded data
                sb.append(nextChar);
            } else if (nextByte == PLUS) {
                // encoded space
                sb.append(nextChar);
                state.paramDecodeRequired = state.urlDecodeRequired = decode;
            } else if (nextByte == EQUALS && state.paramName == null) {
                // query parameter key read complete
                state.paramName = decode(sb.substring(state.position), state.paramDecodeRequired, state, true, true);
                sb.append(nextChar);
                state.position = sb.length();
                state.paramDecodeRequired = false;
            } else if (nextByte == AMPERSAND) {
                // query parameter value read complete
                addParam(state, builder);
                sb.append(nextChar);
                state.position = sb.length();
                state.paramName = null;
                state.paramDecodeRequired = false;
            } else if (isPathSegmentChar(nextByte) || nextByte == SLASH || nextByte == QUESTION) {
                // query parameters data continues
                sb.append(nextChar);
            } else if (nextByte == SPACE) {
                // end of request target
                addParam(state, builder);
                addQuery(state, builder);
                // prepare for next parsing phase
                state.setNext(VERSION);
                return;
            } else if (isRequestTargetChar(nextByte)) {
                // RFC 3986, 2.2 Reserved Characters
                // If a reserved character is found in a URI component and no delimiting role is known for that character, then
                // it must be interpreted as representing the data octet corresponding to that character's encoding in US-ASCII.
                sb.append(nextChar);
            } else if (allowUnescapedCharactersInUrl) {
                // unescaped characters - violating HTTP 1.1 specification
                sb.append(nextChar);
                state.paramDecodeRequired = state.urlDecodeRequired = decode;
            } else throw new BadRequestException();
        }
    }

    private boolean isPercentEncodedSequenceChar(final byte nextByte, final RequestState state) throws BadRequestException {
        if (!isHexDigitChar(state.previousByte) && nextByte == PERCENT) {
            // percent encoded sequence start
            state.urlDecodeRequired = decode;
            if (state.substate == PATH_PARAMS || state.substate == QUERY_PARAMS) state.paramDecodeRequired = decode;
            state.previousByte = nextByte;
        } else if (state.previousByte == PERCENT && isHexDigitChar(nextByte)) {
            // percent encoded sequence continues
            state.previousByte = nextByte;
        } else if (isHexDigitChar(state.previousByte) && isHexDigitChar(nextByte)) {
            // percent encoded sequence end
            state.previousByte = 0;
        } else if (state.previousByte == PERCENT || isHexDigitChar(state.previousByte)) {
            // wrong percent encoded sequence
            throw new BadRequestException();
        } else return false;
        return true;
    }

    private boolean isDoubleDotSegmentCandidate(final byte nextByte, final RequestState state) {
        if (state.doubleDotSegment && nextByte == SEMICOLON) return false; // special case, see UNDERTOW-2339
        if (nextByte == SLASH) {
            state.previousByte = nextByte;
            state.doubleDotSegment = false;
        } else if (state.previousByte == SLASH && nextByte == DOT) {
            state.previousByte = nextByte;
        } else if (state.previousByte == DOT && nextByte == DOT) {
            state.previousByte = 0;
            state.doubleDotSegment = true;
        } else return state.doubleDotSegment = false;
        return true;
    }

    private String decode(final String value, boolean urlDecodeRequired, RequestState state, final boolean slashDecodingFlag, final boolean formEncoded) {
        if (urlDecodeRequired) {
            return URLUtils.decode(value, charset, slashDecodingFlag, formEncoded, state.decodedData);
        } else {
            return value;
        }
    }

    private void addPaths(final RequestState state, final HttpServerExchange exchange) {
        final boolean containsHost = state.targetType == ABSOLUTE_FORM || state.targetType == AUTHORITY_FORM;
        final String path = state.parsedData.toString();
        final String canonicalPath = state.targetType == AUTHORITY_FORM ? path : state.canonicalPath.append(path.substring(state.position)).toString();
        final String requestPath = decode(canonicalPath.isEmpty() && !containsHost ? path : canonicalPath, state.urlDecodeRequired, state, slashDecodingFlag, false);
        exchange.setRequestPath(requestPath.isEmpty() ? "/" : requestPath);
        exchange.setRelativePath(requestPath.isEmpty() ? "/" : requestPath);
        if (state.urlDecodeRequired && allowUnescapedCharactersInUrl) {
            final String uri = decode(path, state.urlDecodeRequired, state, slashDecodingFlag, false);
            exchange.setRequestURI(uri, containsHost);
        } else {
            exchange.setRequestURI(path, containsHost);
        }
    }

    private void addParam(final RequestState state, final HttpServerExchange exchange) throws BadRequestException {
        if (state.paramName == null && state.position == state.parsedData.length()) return;
        final boolean isQueryParam = state.substate == QUERY_PARAMS;
        if (++state.count > maxParameters) {
            if (isQueryParam) throw UndertowMessages.MESSAGES.tooManyQueryParameters(maxParameters);
            else throw UndertowMessages.MESSAGES.tooManyPathParameters(maxParameters);
        }

        final String decodedValue = decode(state.parsedData.substring(state.position), state.paramDecodeRequired, state, true, true);
        final String name = state.paramName == null ? decodedValue : state.paramName;
        final String value = state.paramName == null ? "" : decodedValue;
        if (isQueryParam) {
            exchange.addQueryParam(name, value);
        } else {
            for (String v : value.split(",")) {
                exchange.addPathParam(name, v);
            }
        }
    }

    private void addQuery(final RequestState state, final HttpServerExchange exchange) throws BadRequestException {
        String queryString = state.parsedData.toString();
        exchange.setQueryString(queryString);
        if (state.urlDecodeRequired && allowUnescapedCharactersInUrl) {
            queryString = decode(queryString, state.urlDecodeRequired, state, slashDecodingFlag, false);
            exchange.setDecodedQueryString(queryString);
        }
    }

    private void addHeader(final RequestState state, final HttpServerExchange builder, final String headerValue) throws BadRequestException {
        if (++state.count > maxHeaders) {
            throw new BadRequestException(UndertowMessages.MESSAGES.tooManyHeaders(maxHeaders));
        }

        builder.getRequestHeaders().add(state.headerName, headerValue);
    }
}