io.servicetalk.http.netty.HttpRequestEncoder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of servicetalk-http-netty Show documentation
Show all versions of servicetalk-http-netty Show documentation
A networking framework that evolves with your application
The newest version!
/*
* Copyright © 2018 Apple Inc. and the ServiceTalk project authors
*
* 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.
*/
/*
* Copyright 2012 The Netty Project
*
* The Netty Project licenses this file to you 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.servicetalk.http.netty;
import io.servicetalk.buffer.api.Buffer;
import io.servicetalk.concurrent.internal.QueueFullException;
import io.servicetalk.http.api.HttpRequestMetaData;
import io.servicetalk.http.api.HttpRequestMethod;
import io.servicetalk.http.netty.HttpResponseDecoder.Signal;
import io.servicetalk.transport.netty.internal.CloseHandler;
import io.servicetalk.transport.netty.internal.DefaultNettyConnection.CancelWriteUserEvent;
import io.servicetalk.transport.netty.internal.DefaultNettyConnection.ContinueUserEvent;
import io.netty.channel.ChannelHandlerContext;
import java.util.Queue;
import static io.netty.handler.codec.http.HttpConstants.SP;
import static io.servicetalk.http.api.HttpProtocolVersion.HTTP_1_1;
import static io.servicetalk.http.netty.HeaderUtils.REQ_EXPECT_CONTINUE;
import static io.servicetalk.http.netty.HeaderUtils.shouldAddZeroContentLength;
import static io.servicetalk.http.netty.HttpResponseDecoder.Signal.REQUEST_SIGNAL;
import static io.servicetalk.http.netty.HttpResponseDecoder.Signal.REQUEST_WITH_EXPECT_CONTINUE_SIGNAL;
import static java.util.Objects.requireNonNull;
final class HttpRequestEncoder extends HttpObjectEncoder {
private static final char SLASH = '/';
private static final char QUESTION_MARK = '?';
private static final int SLASH_AND_SPACE_SHORT = (SLASH << 8) | SP;
private static final int SPACE_SLASH_AND_SPACE_MEDIUM = (SP << 16) | SLASH_AND_SPACE_SHORT;
private final Queue methodQueue;
private final Queue signalsQueue;
/**
* Used to remember when an outgoing request has "Expect: 100-continue" header.
*/
private boolean expectContinue;
/**
* Create a new instance.
* @param methodQueue A queue used to enforce HTTP protocol semantics related to request/response lengths.
* @param signalsQueue A queue used to communicate extra signals between encoder and decoder.
* @param headersEncodedSizeAccumulator Used to calculate an exponential moving average of the encoded size of the
* initial line and the headers for a guess for future buffer allocations.
* @param trailersEncodedSizeAccumulator Used to calculate an exponential moving average of the encoded size of
* the trailers for a guess for future buffer allocations.
* @param closeHandler observes protocol state events
*/
HttpRequestEncoder(final Queue methodQueue, final Queue signalsQueue,
final int headersEncodedSizeAccumulator, final int trailersEncodedSizeAccumulator,
final CloseHandler closeHandler) {
super(headersEncodedSizeAccumulator, trailersEncodedSizeAccumulator, closeHandler);
this.methodQueue = requireNonNull(methodQueue);
this.signalsQueue = requireNonNull(signalsQueue);
}
@Override
protected void sanitizeHeadersBeforeEncode(final HttpRequestMetaData msg, final boolean isAlwaysEmpty) {
// This method has side effects on the methodQueue for the following reasons:
// - createMessage will not necessarily fire a message up the pipeline.
// - the trigger points on the queue are currently symmetric for the request/response decoder and
// request/response encoder. We may use header information on the response decoder side, and the queue
// interaction is conditional (1xx responses don't touch the queue).
// - unit tests exist which verify these side effects occur, so if behavior of the internal classes changes the
// unit test should catch it.
// - this is the rough equivalent of what is done in Netty in terms of sequencing. Instead of trying to
// iterate a decoded list it makes some assumptions about the base class ordering of events.
methodQueue.add(msg.method());
}
@Override
protected HttpRequestMetaData castMetaData(Object msg) {
return (HttpRequestMetaData) msg;
}
@Override
protected void encodeInitialLine(ChannelHandlerContext ctx, Buffer stBuffer, HttpRequestMetaData message) {
message.method().writeTo(stBuffer);
String uri = message.requestTarget();
if (uri.isEmpty()) {
// Add " / " as absolute path if uri is not present.
// See https://tools.ietf.org/html/rfc2616#section-5.1.2
stBuffer.writeMedium(SPACE_SLASH_AND_SPACE_MEDIUM);
} else {
CharSequence uriCharSequence = uri;
boolean needSlash = false;
int start = uri.indexOf("://");
if (start != -1 && uri.charAt(0) != SLASH) {
start += 3;
// Correctly handle query params.
// See https://github.com/netty/netty/issues/2732
int index = uri.indexOf(QUESTION_MARK, start);
if (index == -1) {
if (uri.lastIndexOf(SLASH) < start) {
needSlash = true;
}
} else {
if (uri.lastIndexOf(SLASH, index) < start) {
// TODO(scott): ByteBuf to support writing a sub-section of CharSequence?
uriCharSequence = new StringBuilder(uri.length() + 1).append(uri).insert(index, SLASH);
}
}
}
stBuffer.writeByte(SP);
stBuffer.writeUtf8(uriCharSequence);
if (needSlash) {
// write "/ " after uri
stBuffer.writeShort(SLASH_AND_SPACE_SHORT);
} else {
stBuffer.writeByte(SP);
}
}
// It is possible for the user to generate a request with a non-http/1.x version (e.g. ALPN prefers h2), and
// if this happens just force http/1.1 to avoid generating an invalid request.
(message.version().major() == 1 ? message.version() : HTTP_1_1).writeTo(stBuffer);
stBuffer.writeShort(CRLF_SHORT);
}
@Override
protected long getContentLength(final HttpRequestMetaData message) {
final long len = HttpObjectDecoder.getContentLength(message);
// If there is no length header, and we omitted adding one previously because the method isn't expected to have
// content-length then assume the length is 0. If the user does attempt to write content they will get an error
// and should explicitly set content-length.
return len < 0 && !shouldAddZeroContentLength(message.method()) ? 0 : len;
}
@Override
protected void onMetaData(final ChannelHandlerContext ctx, final HttpRequestMetaData metaData) {
expectContinue = REQ_EXPECT_CONTINUE.test(metaData);
// Offer signals for all request types. Otherwise, decoder may receive a wrong signal if client sends multiple
// pipelined requests.
if (!signalsQueue.offer(expectContinue ? REQUEST_WITH_EXPECT_CONTINUE_SIGNAL : REQUEST_SIGNAL)) {
throw new QueueFullException("Can not enqueue a request type for the decoder");
}
}
@Override
public void userEventTriggered(final ChannelHandlerContext ctx, final Object evt) {
if (expectContinue) {
if (evt == ContinueUserEvent.INSTANCE) {
// Reset the flag after receiving 100 (Continue).
expectContinue = false;
} else if (evt == CancelWriteUserEvent.INSTANCE) {
// Mark as content consumed and reset the flag to prepare for the next request on the same connection.
contentLenConsumed(ctx, null);
expectContinue = false;
}
}
ctx.fireUserEventTriggered(evt);
}
}