Request and Response model
Whether accessing querystring parameters, form data or headers, a number of convenient methods exist to get the values, or return defaults, or convert to numbers or some other types.
Contents:
- Query Strings
- Form Data
- Uploads
- Headers
- Cookies
- Matrix parameters
- Request bodies
- Responses
- Response Bodies
Query Strings
These can all be accessed from the Request object by calling request.query()
which returns a RequestParameters object.
public class QueryStringExampleHandler implements RouteHandler {
public void handle(MuRequest request, MuResponse response, Map<String,String> pathParams) {
// Returns null if there is no parameter with that value
String something = request.query().get("something");
// Specifying a default value:
String somethingElse = request.query().get("something", "default value");
// Getting a list, e.g. for ?something=value1&something=value2
List<String> somethingList = request.query().getAll("something");
// Gets a number, or returns the default value if it's missing or not a number.
// There are also getFloat, getDouble, getLong and getBoolean methods
int intValue = request.query().getInt("something", 42);
response.sendChunk(something + " / " + somethingElse + " / " + somethingList + " / " + intValue);
// You can loop through all query string values
for (Map.Entry<String, List<String>> entry : request.query().all().entrySet()) {
response.sendChunk('\n' + entry.getKey() + "=" + entry.getValue());
}
}
}
(see full file)
Try it: Run the above code in your browser.
Form data
Calling request.form()
returns a RequestParameters object
so all values are the same as for query strings:
public class FormDataExampleHandler implements RouteHandler {
public void handle(MuRequest request, MuResponse response, Map<String,String> pathParams) throws IOException {
// Returns null if there is no parameter with that value
String something = request.form().get("something");
// Specifying a default value:
String somethingElse = request.form().get("something", "default value");
// Gets a number, or returns the default value if it's missing or not a number.
// There are also getFloat, getDouble, getLong and getBoolean methods
int intValue = request.form().getInt("numberValue", 42);
boolean checkboxValue = request.form().getBoolean("checkboxValue");
response.sendChunk(something + "\n" + somethingElse + "\n" + intValue + "\n" + checkboxValue);
// You can loop through all the form data
response.sendChunk("\n\nLooping through all form values:");
for (Map.Entry<String, List<String>> entry : request.form().all().entrySet()) {
response.sendChunk('\n' + entry.getKey() + "=" + entry.getValue());
}
}
}
(see full file)
Try it: post some data to the server:
Uploads
Accessing the fields in multi-part form data is as above. To access uploaded files, see the Uploads documentation.
Headers
Request headers share most of the same getters as the query string and form data, however more options are available, including the ability to parse certain complex headers. See the Headers javadoc for more details.
public class HeaderExampleHandler implements RouteHandler {
public void handle(MuRequest request, MuResponse response, Map<String,String> pathParams) {
// Sending headers on the response:
// Set a response header value:
response.headers().set("X-Example-Time", new Date().toString());
// Or add multiple values:
response.headers().add("X-List", "Xavier");
response.headers().add("X-List", "Wolf guy");
// Getting headers from the request:
// Returns null if there is no parameter with that value
String userAgent = request.headers().get("User-Agent");
// Specifying a default value:
String defaultValue = request.headers().get("something", "default value");
// Getting a list where a header has been added multiple times
List<String> somethingList = request.headers().getAll("something");
// Gets a number, or returns the default value if it's missing or not a number.
int intValue = request.headers().getInt("something", 42);
response.sendChunk(userAgent + "\n" + defaultValue + "\n" + somethingList + "\n" + intValue + "\n\n");
// Parsers exist for some header values
List<ParameterizedHeaderWithValue> accepts = request.headers().accept();
for (ParameterizedHeaderWithValue accept : accepts) {
String q = accept.parameter("q", "1.0");
response.sendChunk("Accept " + accept.value() + " with q=" + q + "\n");
}
List<ParameterizedHeaderWithValue> acceptLanguages = request.headers().acceptLanguage();
for (ParameterizedHeaderWithValue acceptLanguage : acceptLanguages) {
String q = acceptLanguage.parameter("q", "1.0");
response.sendChunk("Accept language: " + acceptLanguage.value() + " with q=" + q + "\n");
}
MediaType mediaType = request.headers().contentType();
response.sendChunk("The content type of the request body is " + mediaType + "\n");
ParameterizedHeader cacheControl = request.headers().cacheControl();
response.sendChunk("Cache control: " + cacheControl.parameters() + "\n");
List<ForwardedHeader> forwarded = request.headers().forwarded();
response.sendChunk("Forwarded headers: " + ForwardedHeader.toString(forwarded) + "\n");
// Or just loop through everything
response.sendChunk("\n\nLooping through all headers:\n");
for (Map.Entry<String, String> entry : request.headers()) {
response.sendChunk(entry.getKey() + "=" + entry.getValue() + "\n");
}
}
}
(see full file)
Try it: Run the above code in your browser.
Note that the HeaderNames and HeaderValues classes contain a number of useful constants.
Cookies
Use the CookieBuilder class to create a new cookie and send
it to the client with response.addCookie(Cookie)
.
Cookies can be read with request.cookie(String name)
which returns an
Optional<String>
with the cookie value as the value if present:
public class CookieExampleHandler implements RouteHandler {
public void handle(MuRequest request, MuResponse response, Map<String,String> pathParams) {
switch (request.query().get("action", "view")) {
case "view":
Optional<String> cookie = request.cookie("mu-example-cookie");
String cookieValue = cookie.isPresent() ? cookie.get() : "(no cookie)";
response.write("Cookie value from request is " + cookieValue);
break;
case "create":
Cookie newCookie = CookieBuilder.newCookie()
.withName("mu-example-cookie")
.withValue("CookieValue" + System.currentTimeMillis())
.withMaxAgeInSeconds(45)
.withPath("/model") // only send over /model/*
.secure(false) // only send over HTTPS
.httpOnly(true) // disable JavaScript access
.withSameSite("Lax") // Strict, Lax, or None
.build();
response.addCookie(newCookie);
response.write("Added cookie: " + newCookie);
break;
case "delete":
// The name, path, and domain values must match the created-cookie values
Cookie toDelete = CookieBuilder.newCookie()
.withName("mu-example-cookie")
.withValue("")
.withMaxAgeInSeconds(0)
.withPath("/model")
.build();
response.addCookie(toDelete);
response.write("Deleted cookie " + toDelete);
break;
}
}
}
(see full file)
Try it: Create a cookie and view the cookie and then delete the cookie.
When creating cookies, it is recommended to set SameSite
to Strict
, and enable
Secure
and HTTPOnly
to prevent a number of common
attacks. The CookieBuilder's newSecureCookie()
method creates a build with these values set.
It's important to note that cookie names and values can only use a limited subset of ASCII characters, so
you cannot send arbitrary data (such as JSON) as a cookie value without encoding it first. One option to
allow arbitrary data is to URL Encode the value. The withUrlEncodedValue
method can be used
in place of the withValue
method to achieve this (note the consumer
of the cookie needs to URL decode the value when accessing it.)
If a max age is not set, then the cookie will be a "session" cookie which means browsers may keep
the cookie until the user closes their browser (or not, depending on the browser). To create a persistent
cookie, use the withMaxAgeInSeconds
with a positive number.
Matrix Parameters
URL paths can have attributes associated with each segment of the path. For example, in the following URL, there
are 3 path segments (search
, hotels
, and suites
). The hotels
segment
has 2 attributes: rating
which has two values (4
and 5
) and hasPool
,
whereas the suites
segment just has a single attribute.
/search/hotels;rating=5;rating=4;hasPool=true/suites;beds=2
The following example shows how to locate a segment and extract the parameters for that segment:
public class MatrixParameterExampleHandler implements RouteHandler {
public void handle(MuRequest request, MuResponse response, Map<String, String> pathParams) {
PathMatch matcher = UriPattern.uriTemplateToRegex("/search/{type}/{subtype}").matcher(request.uri());
if (matcher.fullyMatches()) {
PathSegment typeSegment = matcher.segments().get("type");
response.sendChunk("Search type=" + typeSegment.getPath());
for (String rating : typeSegment.getMatrixParameters().get("rating")) {
response.sendChunk("; rating=" + rating);
}
String hasPool = typeSegment.getMatrixParameters().getFirst("hasPool");
response.sendChunk("; hasPool=" + hasPool + "\n\n");
PathSegment subTypeSegment = matcher.segments().get("subtype");
String beds = subTypeSegment.getMatrixParameters().getFirst("beds");
response.sendChunk("Subtype=" + subTypeSegment.getPath() + " with beds=" + beds);
} else {
response.write("URL didn't match");
}
}
}
(see full file)
Request bodies
While form data and uploads can be accessed via the request object, there are other ways to read request bodies.
Calling request.readBodyAsString()
returns the request body as a string. This is
convenient if you are expecting a small text upload, however for binary or large uploads it is better to
stream the data.
The inputStream()
method on the request object is an Optional
value which
has a readable input stream if the client sent a body:
Optional<InputStream> inputStreamOption = request.inputStream();
if (inputStreamOption.isPresent()) {
InputStream inputStream = inputStreamOption.get();
// read the stream
}
Request bodies can also be read asynchronously.
The content type of the request data can be found by calling request.headers().contentType()
which returns a MediaType object
which has getType()
and getSubtype()
methods.
Responses
The MuResponse javadocs have detailed information on handling responses. Generally speaking though, a status, content-type, and headers can be set, followed by the response body.
The status can be set with response.status(int)
which defaults to 200
if not set.
The content type can be set with response.contentType(String)
which defaults to text/plain
if text is sent; otherwise application/octet-stream
is used. Note that the
ContentTypes class defines constants for many content types.
See the sections above for setting headers and cookies.
Response bodies
There are several options for sending response bodies:
- Writing a single string as a full response
- Sending multiple strings, chunk by chunk
- Writing text to a PrintWriter
- Writing binary data to an output stream
- Writing byte-buffers asynchronously
Writing a single string as a full response
This is a good option if you have some text to send and you don't require streaming:
response.write("Hello, world");
This will send a fixed-length response to the client. Note you can only call write
once per
response, so if you find you want to call it multiple times, use one of the following options.
Sending multiple strings, chunk by chunk
The sendChunk(String)
method will immediately send the text to the client and can be called
multiple times:
public class SendChunkExample {
public static void main(String[] args) {
MuServer server = MuServerBuilder.httpServer()
.addHandler(Method.GET, "/", (request, response, path) -> {
response.contentType("text/html;charset=utf-8");
response.sendChunk("Will send some chunks...<br>");
for (int i = 0; i < 10; i++) {
response.sendChunk("Chunk " + i + "<br>");
Thread.sleep(500);
}
response.sendChunk("<br>Response complete.<br>");
})
.start();
System.out.println("See some chunks at " + server.uri());
}
}
(see full file)
Writing text to a PrintWriter
The response.writer()
method returns a buffered UTF-8 java.io.PrintWriter
.
You can optionally call the flush()
method on the writer to immediately send the contents.
public class ResponseTextWriterExample {
public static void main(String[] args) {
MuServer server = MuServerBuilder.httpServer()
.addHandler(Method.GET, "/", (request, response, path) -> {
response.contentType("text/html;charset=utf-8");
try (PrintWriter writer = response.writer()) {
writer.append("Will send some messages...<br>");
for (int i = 0; i < 10; i++) {
writer.append("Sending message " + i + "<br>");
writer.flush();
Thread.sleep(500);
}
writer.append("<br>Response complete.<br>");
}
})
.start();
System.out.println("See some streaming at " + server.uri());
}
}
(see full file)
Writing binary data to an output stream
To sent raw bytes to the client, response.outputStream()
can be used which is a buffered
OutputStream
. Similar to the PrintStream
, calling flush()
is optional and
will immediately send the buffered contents to the client.
In this example, an InputStream
is found which is a PNG on the classpath, and as the data from the image
is read it is written to the response output stream:
public class ResponseOutputStreamExample {
public static void main(String[] args) {
MuServer server = MuServerBuilder.httpServer()
.addHandler(Method.GET, "/logo.png", (request, response, path) -> {
response.contentType(ContentTypes.IMAGE_PNG);
try (
InputStream inputStream = ResponseOutputStreamExample.class
.getResourceAsStream("/web/logos/android-chrome-512x512.png");
OutputStream responseStream = response.outputStream()) {
byte[] buffer = new byte[8192];
int read;
while ((read = inputStream.read(buffer)) > -1) {
responseStream.write(buffer, 0, read);
}
}
})
.start();
System.out.println("Download the logo at " + server.uri().resolve("/logo.png"));
}
}
(see full file)
The PrintStream
and OutputStream
options are often useful if needing to integrate
with other libraries that expect these types of objects.
Note: you should generally use a built-in static resource handler if you want to serve files from the filesystem or the classpath.
Writing byte-buffers asynchronously
When a request is being handled asynchronously, an AsyncHandle is returned which has non-blocking methods for writing ByteBuffer
objects. This can be a good choice
when responses may be long running as it will reduce the number of threads use, which is what the built-in
static file handler does.
See the async handling documentation for more information.