Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add reponse_body to be able to provide a custom body (issue 117) #120

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/index.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,17 @@ invalid credentials (401), internal errors (503) or backpressure (429).

If 204 (No Content) is set, the response body will not be sent in the response.

[id="plugins-{type}s-{plugin}-response_body"]
===== `response_body`

* Value type is: <<string,string>>
* Default value is `ok`

The response body if the request is processed successfully.

The response body is not validated and the `Content-type` is not adjusted to match its actual type.
No body will be sent if `response_code` is configured to 204 (No Content).

[id="plugins-{type}s-{plugin}-response_headers"]
===== `response_headers`

Expand Down
4 changes: 3 additions & 1 deletion lib/logstash/inputs/http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ class LogStash::Inputs::Http < LogStash::Inputs::Base
config :max_content_length, :validate => :number, :required => false, :default => 100 * 1024 * 1024

config :response_code, :validate => [200, 201, 202, 204], :default => 200

config :response_body, :validate => :string, :default => "ok"
# Deprecated options

# The JKS keystore to validate the client's certificates
Expand Down Expand Up @@ -211,7 +213,7 @@ def validate_ssl_settings!

def create_http_server(message_handler)
org.logstash.plugins.inputs.http.NettyHttpServer.new(
@host, @port, message_handler, build_ssl_params(), @threads, @max_pending_requests, @max_content_length, @response_code)
@host, @port, message_handler, build_ssl_params(), @threads, @max_pending_requests, @max_content_length, @response_code, @response_body)
end

def build_ssl_params
Expand Down
49 changes: 49 additions & 0 deletions spec/inputs/http_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,55 @@
expect(response.code).to eq(202)
end
end
context "when response_code is set to 204" do
let(:code) { 204 }
subject { LogStash::Inputs::Http.new("port" => port, "response_code" => code) }
it "responds with the configured code and no body even if forced" do
response = client.post("http://127.0.0.1:#{port}", :body => "hello")
response.call
expect(response.code).to eq(204)
expect(response.body).to eq(nil)
end
end
end
describe "return body" do
context "when response_body is not configured" do
subject { LogStash::Inputs::Http.new("port" => port) }
it "responds with the default body" do
response = client.post("http://127.0.0.1:#{port}", :body => "hello")
response.call
expect(response.body).to eq("ok")
end
end
context "when response_body is configured" do
let(:body) { "world!" }
subject { LogStash::Inputs::Http.new("port" => port, "response_body" => body) }
it "responds with the configured body" do
response = client.post("http://127.0.0.1:#{port}", :body => "hello")
response.call
expect(response.body).to eq(body)
end
end
context "when response_body is configured to an empty string" do
let(:body) { "" }
subject { LogStash::Inputs::Http.new("port" => port, "response_body" => body) }
it "responds with the configured body" do
response = client.post("http://127.0.0.1:#{port}", :body => "hello")
response.call
expect(response.body).to eq(body)
end
end
context "when response_body is configured and content-type is specified" do
let(:body) { "{\"test\": \"body\"}" }
let(:custom_headers) { { 'content-type' => "application/json" } }
subject { LogStash::Inputs::Http.new("port" => port, "response_body" => body, "response_headers" => custom_headers) }
it "responds with the configured body and headers" do
response = client.post("http://127.0.0.1:#{port}", :body => "Plain-text")
response.call
expect(response.body).to eq(body)
expect(response.headers.to_hash).to include({ "content-type" => "application/json" })
end
end
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ public class HttpInitializer extends ChannelInitializer<SocketChannel> {
private final int maxContentLength;
private final HttpResponseStatus responseStatus;
private final ThreadPoolExecutor executorGroup;
private final String responseBody;

public HttpInitializer(IMessageHandler messageHandler, ThreadPoolExecutor executorGroup,
int maxContentLength, HttpResponseStatus responseStatus) {
int maxContentLength, HttpResponseStatus responseStatus,
String responseBody) {
this.messageHandler = messageHandler;
this.executorGroup = executorGroup;
this.maxContentLength = maxContentLength;
this.responseStatus = responseStatus;
this.responseBody = responseBody;
}

protected void initChannel(SocketChannel socketChannel) throws Exception {
Expand All @@ -40,7 +43,7 @@ protected void initChannel(SocketChannel socketChannel) throws Exception {
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpContentDecompressor());
pipeline.addLast(new HttpObjectAggregator(maxContentLength));
pipeline.addLast(new HttpServerHandler(messageHandler.copy(), executorGroup, responseStatus));
pipeline.addLast(new HttpServerHandler(messageHandler.copy(), executorGroup, responseStatus, responseBody));
}

public void enableSSL(SslHandlerProvider sslHandlerProvider) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,21 @@ public class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpReque
private final IMessageHandler messageHandler;
private final ThreadPoolExecutor executorGroup;
private final HttpResponseStatus responseStatus;
private final String responseBody;

public HttpServerHandler(IMessageHandler messageHandler, ThreadPoolExecutor executorGroup,
HttpResponseStatus responseStatus) {
HttpResponseStatus responseStatus, String responseBody) {
this.messageHandler = messageHandler;
this.executorGroup = executorGroup;
this.responseStatus = responseStatus;
this.responseBody = responseBody;
}

@Override
public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) {
final String remoteAddress = ((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().getHostAddress();
msg.retain();
final MessageProcessor messageProcessor = new MessageProcessor(ctx, msg, remoteAddress, messageHandler, responseStatus);
final MessageProcessor messageProcessor = new MessageProcessor(ctx, msg, remoteAddress, messageHandler, responseStatus, responseBody);
executorGroup.execute(messageProcessor);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,18 @@ public class MessageProcessor implements RejectableRunnable {
private final String remoteAddress;
private final IMessageHandler messageHandler;
private final HttpResponseStatus responseStatus;
private final String responseBody;
private static final Charset charset = Charset.forName("UTF-8");

MessageProcessor(ChannelHandlerContext ctx, FullHttpRequest req, String remoteAddress,
IMessageHandler messageHandler, HttpResponseStatus responseStatus) {
IMessageHandler messageHandler, HttpResponseStatus responseStatus,
String responseBody) {
this.ctx = ctx;
this.req = req;
this.remoteAddress = remoteAddress;
this.messageHandler = messageHandler;
this.responseStatus = responseStatus;
this.responseBody = responseBody;
}

public void onRejection() {
Expand Down Expand Up @@ -88,9 +91,8 @@ private FullHttpResponse generateResponse(Map<String, String> stringHeaders) {
response.headers().set(headers);

if (responseStatus != HttpResponseStatus.NO_CONTENT) {
final ByteBuf payload = Unpooled.wrappedBuffer("ok".getBytes(charset));
final ByteBuf payload = Unpooled.wrappedBuffer(this.responseBody.getBytes(charset));
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, payload.readableBytes());
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of removing the response's content-type we could also provide an customization option for it.

Close to response_body it could be also defined a new response_content_type which by default contains text/plain but in case the user could also modify it.
In particular on registration phase, if the user customized the response body then it should be also forced to explicitly define also a response's content-type

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep it's a nice idea. I hope I'll have a chance to resume working on this 😅

response.content().writeBytes(payload);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ public class NettyHttpServer implements Runnable, Closeable {

public NettyHttpServer(String host, int port, IMessageHandler messageHandler,
SslHandlerProvider sslHandlerProvider, int threads,
int maxPendingRequests, int maxContentLength, int responseCode)
int maxPendingRequests, int maxContentLength, int responseCode,
String responseBody)
{
this.host = host;
this.port = port;
Expand All @@ -44,7 +45,7 @@ public NettyHttpServer(String host, int port, IMessageHandler messageHandler,
new CustomRejectedExecutionHandler());

final HttpInitializer httpInitializer = new HttpInitializer(messageHandler, executorGroup,
maxContentLength, responseStatus);
maxContentLength, responseStatus, responseBody);

if (sslHandlerProvider != null) {
httpInitializer.enableSSL(sslHandlerProvider);
Expand Down