I'm trying to implement filter for logging requests and responses in Spring MVC application.
I use the following code:
#Component
public class LoggingFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(LoggingFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
LOGGER.debug(REQUEST_MESSAGE_FORMAT, requestWrapper.getRequestURI(), requestWrapper.getMethod(), requestWrapper.getContentType(),
new ServletServerHttpRequest(requestWrapper).getHeaders(), IOUtils.toString(requestWrapper.getInputStream(), UTF_8));
filterChain.doFilter(requestWrapper, responseWrapper);
LOGGER.debug(RESPONSE_MESSAGE_FORMAT, responseWrapper.getStatus(), responseWrapper.getContentType(),
new ServletServerHttpResponse(responseWrapper).getHeaders(), IOUtils.toString(responseWrapper.getContentInputStream(), UTF_8));
}
}
So, I get my request and respone logged as expected. Here are the logs:
2016-10-08 19:10:11.212 DEBUG 11072 --- [qtp108982313-19] by.kolodyuk.logging.LoggingFilter
----------------------------
ID: 1
URI: /resources/1
Http-Method: GET
Content-Type: null
Headers: {User-Agent=[curl/7.41.0], Accept=[*/*], Host=[localhost:9015]}
Body:
--------------------------------------
2016-10-08 19:10:11.277 DEBUG 11072 --- [qtp108982313-19] by.kolodyuk.logging.LoggingFilter
----------------------------
ID: 1
Response-Code: 200
Content-Type: application/json;charset=UTF-8
Headers: {}
Body: {"id":"1"}
--------------------------------------
However, the empty response is returned. Here's the output from curl:
$ curl http://localhost:9015/resources/1 --verbose
* Trying ::1...
* Connected to localhost (::1) port 9015 (#0)
> GET /resources/1 HTTP/1.1
> User-Agent: curl/7.41.0
> Host: localhost:9015
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sat, 08 Oct 2016 17:10:11 GMT
< Content-Type: application/json;charset=UTF-8
< Content-Length: 0
<
* Connection #0 to host localhost left intact
Any ideas?
Thanks
After couple of hours of struggling, I've finally found the solution.
In short, ContentCachingResponseWrapper.copyBodyToResponse() should be called in the end of the filter method.
ContentCachingResponseWrapper caches the response body by reading it from response output stream. So, the stream becomes empty. To write response back to the output stream ContentCachingResponseWrapper.copyBodyToResponse() should be used.
Finally solved the problem. Here is the perfect solution:
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.Enumeration;
import java.util.Map;
import static java.nio.charset.StandardCharsets.UTF_8;
import static net.logstash.logback.marker.Markers.appendFields;
#Component
public class LoggingFilter extends OncePerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(LoggingFilter.class);
#Autowired
private ObjectMapper objectMapper;
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper(httpServletRequest);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(httpServletResponse);
filterChain.doFilter(requestWrapper, responseWrapper);
String requestUrl = requestWrapper.getRequestURL().toString();
HttpHeaders requestHeaders = new HttpHeaders();
Enumeration headerNames = requestWrapper.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = (String) headerNames.nextElement();
requestHeaders.add(headerName, requestWrapper.getHeader(headerName));
}
HttpMethod httpMethod = HttpMethod.valueOf(requestWrapper.getMethod());
Map<String, String[]> requestParams = requestWrapper.getParameterMap();
String requestBody = IOUtils.toString(requestWrapper.getInputStream(),UTF_8);
JsonNode requestJson = objectMapper.readTree(requestBody);
RequestEntity<JsonNode> requestEntity = new RequestEntity<>(requestJson,requestHeaders, httpMethod, URI.create(requestUrl));
LOGGER.info(appendFields(requestEntity),"Logging Http Request");
HttpStatus responseStatus = HttpStatus.valueOf(responseWrapper.getStatusCode());
HttpHeaders responseHeaders = new HttpHeaders();
for (String headerName : responseWrapper.getHeaderNames()) {
responseHeaders.add(headerName, responseWrapper.getHeader(headerName));
}
String responseBody = IOUtils.toString(responseWrapper.getContentInputStream(), UTF_8);
JsonNode responseJson = objectMapper.readTree(responseBody);
ResponseEntity<JsonNode> responseEntity = new ResponseEntity<>(responseJson,responseHeaders,responseStatus);
LOGGER.info(appendFields(responseEntity),"Logging Http Response");
responseWrapper.copyBodyToResponse();
}
}
The pattern I like to use is to split this into 2 filters, one for extracting the raw body and another one to do the logging - feels a more SRP.
#Slf4j // lombok logging
#Component // spring loads filter into it's filter chain
#Order(1) // Best if this goes first (or early in filter chain)
public class CachingBodyFilter implements Filter {
#Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
ContentCachingRequestWrapper reqWrapper = new ContentCachingRequestWrapper((HttpServletRequest) req);
ContentCachingResponseWrapper resWrapper = new ContentCachingResponseWrapper((HttpServletResponse) res);
try {
chain.doFilter(reqWrapper, resWrapper);
resWrapper.copyBodyToResponse(); // Necessary (see answer by StasKolodyuk above)
} catch (IOException | ServletException e) {
log.error("Error extracting body", e);
}
}
}
And then we create another filter to do the logging part.
#Slf4j
#Component
#Order(2) // This needs to come after `CachingBodyFilter`
public class PayloadLogFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
chain.doFilter(req, res);
if (req instanceof ContentCachingRequestWrapper) {
ContentCachingRequestWrapper reqWrapper = (ContentCachingRequestWrapper) req;
String payload = new String (reqWrapper.getContentAsByteArray(), "utf-8");
log.debug("Request [ {} ] has payload [ {} ]", reqWrapper.getRequestURI(), payload);
}
}
}
A nice advantage of splitting these up is that other classes (e.g. a Spring AOP interceptor or a Spring controller) can also access / use the HTTP body.
Related
I was wondering if any you can point me to a web flutter library that had http badCertificateCallback. I tried DIO but it is giving me an error and submit an issue but I haven't heard from them yet
DIO code:
Dio dio = new Dio(options);
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(client) {
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
};
Error: Expected a value of type 'DefaultHttpClientAdapter', but got one of type 'BrowserHttpClientAdapter'
I also tried http, but it doesn't have a bad Certificate Callback, we could use this but it isn't web-compatible
HttpClient httpClient = new HttpClient();
httpClient.badCertificateCallback =
((X509Certificate cert, String host, int port) => true);
IOClient ioClient = new IOClient(httpClient);
response = await ioClient.post(url, body: data, headers: headers);
Any comment will be more that apreciate.
Thanks in advance,
Daniel
Make a http Client like this,
import 'dart:convert';
import 'dart:io';
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'dart:io' as IO;
....
....
....
/// CLIENT
static Future<Dio> _dioClient() async {
Dio dio = Dio(await _getOptions()); // Getting Headers and Other data
if(!kIsWeb){
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
(IO.HttpClient client) {
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
return client;
};
}
return dio;
}
I use badCertificateCallback with DIO this way:
//import 'package:get/get.dart' hide Response hide FormData; //<-- if you use get package
import 'package:dio/dio.dart';
void main(){
HttpOverrides.global = new MyHttpOverrides();
runApp(MyApp());
}
class MyHttpOverrides extends HttpOverrides{
#override
HttpClient createHttpClient(SecurityContext context){
return super.createHttpClient(context)
..badCertificateCallback = ((X509Certificate cert, String host, int port) {
final isValidHost = ["192.168.1.67"].contains(host); // <-- allow only hosts in array
return isValidHost;
});
}
}
// more example: https://github.com/flutterchina/dio/tree/master/example
void getHttp() async {
Dio dio = new Dio();
Response response;
response = await dio.get("https://192.168.1.67");
print(response.data);
}
you can just turn this part of your to this one 👇
HttpClient client = new HttpClient();
client.badCertificateCallback =((X509Certificate cert, String host, int port) => true);
I'm trying to make a POST request to an endpoint in Java, and when I try to send the request, I get the following error:
Caused by: javax.net.ssl.SSLHandshakeException: The server selected protocol version TLS10 is not accepted by client preferences [TLS13, TLS12]
This is what I have so far
Map<Object, Object> data = new HashMap<>();
data.put("username","foo");
data.put("password","bar");
String url = "https://google.com";
HttpRequest request = HttpRequest.newBuilder()
.POST(buildFormDataFromMap(data))
.uri(URI.create(url))
.build();
try{
HttpResponse<String> response = httpClient.send(request,
HttpResponse.BodyHandlers.ofString());
System.out.println(response.statusCode());
System.out.println(response.body());
} catch (Exception e){
e.printStackTrace();
}
Then when I run the code, the error gets thrown when sending the request/making the response object. My question is, if the TLS preferences are different for the server than the client, how can I change the preferences within Java so it can still make the request?
To solve this problem in jdk 11, I had to create an javax.net.ssl.SSLParameters object to enable "TLSv1", etc:
SSLParameters sslParameters = new SSLParameters();
sslParameters.setProtocols(new String[]{"TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"});
Then create the HttpClient and add the sslParamters object:
HttpClient httpClient = HttpClient.newBuilder()
.sslParameters(sslParameters)
.build();
If you also want to disable hostname verification, add following code BEFORE HttpClient initialization;
final Properties props = System.getProperties();
props.setProperty("jdk.internal.httpclient.disableHostnameVerification", Boolean.TRUE.toString());
Also you can add a new TrustManager to trust all certificates (self signed).
To do so, add following code into your Class:
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(
java.security.cert.X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(
java.security.cert.X509Certificate[] certs, String authType) {
}
}
};
After this, you have to create an SSLContext object and add the TrustManger object:
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
And finally alter the HttpClient initialization like this:
httpClient = HttpClient.newBuilder()
.sslContext(sslContext)
.sslParameters(sslParameters)
.build()
Here is a complete Class example:
import java.net.http.HttpClient;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.X509Certificate;
import java.util.Properties;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public class HttpSSLClient {
private SSLContext sslContext;
private SSLParameters sslParameters;
private HttpClient httpClient;
public HttpSSLClient() throws KeyManagementException, NoSuchAlgorithmException {
sslParameters = new SSLParameters();
sslParameters.setProtocols(new String[]{"TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"});
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
final Properties props = System.getProperties();
props.setProperty("jdk.internal.httpclient.disableHostnameVerification", Boolean.TRUE.toString());
httpClient = HttpClient.newBuilder()
.sslContext(sslContext)
.sslParameters(sslParameters)
.build();
}
public HttpClient getHttplClient() {
return httpClient;
}
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(
java.security.cert.X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(
java.security.cert.X509Certificate[] certs, String authType) {
}
}
};
}
You can use the getHttplClient() function while calling your HttpRequest.
I had the same issue and this solution does not work for me.
Instead I saw this answer Android Enable TLSv1.2 in OKHttp and I tried this code:
ConnectionSpec spec = new ConnectionSpec
.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2,TlsVersion.TLS_1_0,TlsVersion.TLS_1_1,TlsVersion.TLS_1_3).build();
client =client.newBuilder().connectionSpecs(Collections.singletonList(spec)).build();
And it worked for me:)
I think mmo's answer should be highlighted in bold. I had similar issue, but found out that the open-jdk jvm I was using has TLSv1 and TLSv1.1 as disabled in the jdk.tls.disabledAlgorithms line in java.security. So as soon as I removed it and restarted the JVM, I was able to connect usingthe older TLS protocols.
But please pay ATTENTION, This is not advisable in Production since it degrades the secure communication. So I'd say change it if you want at YOUR OWN RISK!!!
I am trying to test a Servlet with special character which is deployed in jetty 9.
I am posting a String with a single quote character to the Servlet. Though I have mentioned to use UTF-8 character encoding in the Servlet , the Servlet cannot print the single quote character. I don't know what's wrong in the following code :
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class SpecialCharacterServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
BufferedReader in = new BufferedReader(
new InputStreamReader(request.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println(inputLine);
}
in.close();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
The main method
public static void main(String s[]) {
try {
HttpURLConnection conn = (HttpURLConnection) new URL("http://localhost:8080/test/SpecialCharacterServlet").openConnection();
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
conn.setRequestProperty("Connection", "Keep-Alive");
conn.setRequestProperty("Content-Type", "multipart/form-data;boundary=*****");
String str = "Hello ‘World’";
DataOutputStream dos = new DataOutputStream(conn.getOutputStream());
dos.writeBytes(str);
dos.flush();
dos.close();
InputStream is = conn.getInputStream();
byte[] content = new byte[is.available()];
is.read(content);
}catch(Exception e) {
System.out.println(e);
}
}
The output does not show the single quoted character , the output is Hello World , it's some block in there .
You need to remove any action where character sets are implied but not defined.
Ensure that you're Main.class is UTF-8 encoded.
Ensure that javac is aware that Main.class is UTF-8 encoded. Most IDE's do this for you but it requires you to set the encoding in the file properties
In main() wrap the OutputStream with OutputStreamWriter with an explicit charset declaration. This ensures everything written through the OutputStreamWriter is in the correct charset:
osw = OutputStreamWriter(conn.getOutputStream(), "UTF-8");
osw.write(str);
In your servlet, tell your inputStream to decode from "UTF-8":
BufferedReader in = new BufferedReader(
new InputStreamReader(request.getInputStream(), "UTF-8"));
System.out is also subject to charset conversion. This is particularly troublesome on DOS consoles. Instead, write the result to a text file and check the results in a good text editor, such as Notepad++. Again, when writing to the text file, set the character set in the constructor of your writer.
I am having trouble getting a response from the Twitter API. I am using scribe 1.3.5 here. When TwitterLoginServlet is called from one page it successfully redirects me to twitter and allows me to login. However, on the callback, TwitterCallbackServlet receives the following information in the oAuthResponse.
code - 401
message - Unauthorized
body - Failed to validate oauth signature and token
I am new to using both servlets and oauth so it is completely possible I am making some silly mistake in the following code. I believe this is all that is needed to find a solution to the problem but if you need additional information I will be checking this post vigilantly.
Thanks!
public class TwitterServlet extends HttpServlet {
private static final String SESSION_NAME_REQUEST_TOKN = "twitter.requestToken";
protected Token getRequestToken(HttpServletRequest req) {
HttpSession session = req.getSession();
try {
return (Token) session.getAttribute(SESSION_NAME_REQUEST_TOKN);
}
finally {
session.removeAttribute(SESSION_NAME_REQUEST_TOKN);
}
}
protected void setRequestToken(HttpServletRequest req, Token token) {
HttpSession session = req.getSession();
session.setAttribute(SESSION_NAME_REQUEST_TOKN, token);
}
protected OAuthRequest createRequest() {
OAuthRequest request = new OAuthRequest(Verb.GET, "https://api.twitter.com/oauth/request_token");
return request;
}
}
public class TwitterLoginServlet extends TwitterServlet {
#Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String callback = "http://" + request.getServerName() + ":" + request.getServerPort() + "/******/TwitterCallbackServlet";
OAuthService service = new ServiceBuilder().provider(TwitterApi.SSL.class)
.apiKey("******")
.apiSecret("******")
.callback(callback)
.build();
Token requestToken = service.getRequestToken();
setRequestToken(request, requestToken);
response.sendRedirect(service.getAuthorizationUrl(requestToken));
return;
}
#Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
public class TwitterCallbackServlet extends TwitterServlet {
#Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
OAuthService service = new ServiceBuilder().provider(TwitterApi.SSL.class)
.apiKey("******")
.apiSecret("******")
.build();
Token requestToken = getRequestToken(request);
// TODO: Check if the requestToken matches the token of this request.
String verifier = request.getParameter(OAuthConstants.VERIFIER);
Token accessToken = service.getAccessToken(requestToken, new Verifier(verifier));
OAuthRequest oAuthRequest = createRequest();
service.signRequest(accessToken, oAuthRequest);
Response oAuthResponse = oAuthRequest.send();
String body = oAuthResponse.getBody();
response.sendRedirect("/******/accountSettings.xhtml");
}
Why are you hitting the requestToken endpoint again after getting the access token? Try accessing a different resource, for example:
"https://api.twitter.com/1.1/account/verify_credentials.json";
Note that you can run the TwitterExample just to check that stuff is working fine.
Hi I have a simple restlet get method which returns a static string. It looks like the following:
#Get
public String represent() {
return "mystring\r\n";
}
A low level c app is invoking this get by going into a read loop. It never receives a finish confirmation signaling it that there is no more data left to read and times out after 20 seconds. Is there code I need to send to alert the client app that no more data is coming? Or that the get is finished?
[Note: The code written below is partly based on the samples available on http://www.restlet.org ]
HTTP 1.0 and 1.1 have a header named Content-Length. Whatever is the numeric value of that header, is the length of the body of the HTTP-response. Going a step further in HTTP 1.1, there is another header name-value, Tranfer-Encoding: chunked which indicates that the response-body is divided into parts (chunks) and each part's length is mentioned in a line just before that part is delivered.(I am not including other values of Transfer-Encoding to keep this answer concised.)
If this is my restlet server:
package restletapp;
import org.restlet.Component;
import org.restlet.data.Protocol;
import org.restlet.resource.Get;
import org.restlet.resource.ServerResource;
public class RestletApp extends ServerResource {
public static void main(String[] args) throws Exception {
Component component = new Component();
component.getServers().add(Protocol.HTTP, 8182);
component.getDefaultHost().attach("/trace", RestletApp.class);
component.start();
}
#Get
public String toAtGet() {
return "Resource URI : " + getReference() + '\n'
+ "Root URI : " + getRootRef() + '\n'
+ "Routed part : " + getReference().getBaseRef() + '\n'
+ "Remaining part: " + getReference().getRemainingPart()
;
}
}
And this is my client (written using Sockets in Java. Just sends a minimal HTTP request, and prints each character of response on console.)
package restletapp;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
public class Requester {
public static void main(String[] args) throws UnknownHostException, IOException {
Socket s=new Socket("localhost", 8182);
OutputStream os = s.getOutputStream();
os.write((
"GET /trace HTTP/1.1\r\n" //request
+ "host: localhost:8182\r\n" //request
+ "Connection-type: close\r\n\r\n" //request
).getBytes());
InputStream is = s.getInputStream();
for(int ch;(ch=is.read())!=-1;System.out.flush())
System.out.write(ch); //response, one char at a time.
is.close();
os.close();
s.close();
}
}
The client process never ends. But if I change my client program to this:
package restletapp;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
public class Requester {
public static void main(String[] args) throws UnknownHostException, IOException {
Socket s=new Socket("localhost", 8182);
OutputStream os = s.getOutputStream();
os.write((
"GET /trace HTTP/1.1\r\n"
+ "host: localhost:8182\r\n"
+ "Connection-type: close\r\n\r\n"
).getBytes());
InputStream is = s.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
int bytesRead=0;
int contentLength=0;
//response headers.
for(String line;!(line=br.readLine()).isEmpty();System.out.flush()){
System.out.println(line);
String[] tokens = line.split(":| ");
if(tokens[0].equalsIgnoreCase("content-length")){
contentLength=Integer.parseInt(tokens[2]);
}
}
//response separator, between headers and body.
System.out.println();
//response body.
while(bytesRead<contentLength){
System.out.write(br.read());
System.out.flush();
bytesRead++;
}
is.close();
os.close();
s.close();
}
}
In the second version of Requester, you can see that the connection is closed by the client when response body's content-length-number of characters are read.
This is what i get using curl:
command line $ curl -i "http://localhost:8182/trace"
HTTP/1.1 200 OK
Date: Fri, 14 Jun 2013 11:54:32 GMT
Server: Restlet-Framework/2.0.15
Vary: Accept-Charset, Accept-Encoding, Accept-Language, Accept
Content-Length: 148
Content-Type: text/plain; charset=UTF-8
Resource URI : http://localhost:8182/trace
Root URI : http://localhost:8182/trace
Routed part : http://localhost:8182/trace
Remaining part:
command line $
You can see, that curl exits after reading the content.