From f571bd13dcbd648036324997b17de9d20a8bd7dd Mon Sep 17 00:00:00 2001 From: Benoit Sigoure Date: Thu, 28 Oct 2010 00:25:30 -0700 Subject: [PATCH] Make all HTTP endpoints send return PNG-friendly error responses. Similarly to the previous commit, error responses (say, 400, 404, 500), now honor the `png' query string parameter, which indicates that the client wants a PNG image as a response. This was only handled by /q, but is now automagically done for all endpoints. Among other things, this helps report proper error messages to pages that embed an image from the TSD without first doing an AJAX call to see if there's an error. Change-Id: Idb44d3ef78152cfb98c929e355c8f615f893b30f --- src/tsd/GraphHandler.java | 6 ++-- src/tsd/HttpQuery.java | 75 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 6 deletions(-) diff --git a/src/tsd/GraphHandler.java b/src/tsd/GraphHandler.java index 2d1da32849..0e8e8b30f7 100644 --- a/src/tsd/GraphHandler.java +++ b/src/tsd/GraphHandler.java @@ -584,9 +584,9 @@ static void setPlotParams(final HttpQuery query, final Plot plot) { * graph from the file it produces, or if we have been interrupted. * @throws GnuplotException if Gnuplot returns non-zero. */ - private static int runGnuplot(final HttpQuery query, - final String basepath, - final Plot plot) throws IOException { + static int runGnuplot(final HttpQuery query, + final String basepath, + final Plot plot) throws IOException { final int nplotted = plot.dumpToFiles(basepath); final long start_time = System.nanoTime(); final Process gnuplot = new ProcessBuilder( diff --git a/src/tsd/HttpQuery.java b/src/tsd/HttpQuery.java index 3aa1cbfdd3..0186453b2d 100644 --- a/src/tsd/HttpQuery.java +++ b/src/tsd/HttpQuery.java @@ -16,6 +16,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -41,6 +42,7 @@ import org.jboss.netty.handler.codec.http.QueryStringDecoder; import org.jboss.netty.util.CharsetUtil; +import net.opentsdb.graph.Plot; import net.opentsdb.stats.Histogram; import net.opentsdb.stats.StatsCollector; @@ -202,6 +204,8 @@ public void internalError(final Exception cause) { HttpQuery.escapeJson(pretty_exc, buf); buf.append("\"}"); sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, buf); + } else if (hasQueryStringParam("png")) { + sendAsPNG(HttpResponseStatus.INTERNAL_SERVER_ERROR, pretty_exc, 30); } else { sendReply(HttpResponseStatus.INTERNAL_SERVER_ERROR, makePage("Internal Server Error", "Houston, we have a problem", @@ -227,6 +231,8 @@ public void badRequest(final String explain) { HttpQuery.escapeJson(explain, buf); buf.append("\"}"); sendReply(HttpResponseStatus.BAD_REQUEST, buf); + } else if (hasQueryStringParam("png")) { + sendAsPNG(HttpResponseStatus.BAD_REQUEST, explain, 3600); } else { sendReply(HttpResponseStatus.BAD_REQUEST, makePage("Bad Request", "Looks like it's your fault this time", @@ -247,6 +253,8 @@ public void notFound() { if (hasQueryStringParam("json")) { sendReply(HttpResponseStatus.NOT_FOUND, new StringBuilder("{\"err\":\"Page Not Found\"}")); + } else if (hasQueryStringParam("png")) { + sendAsPNG(HttpResponseStatus.NOT_FOUND, "Page Not Found", 3600); } else { sendReply(HttpResponseStatus.NOT_FOUND, PAGE_NOT_FOUND); } @@ -399,17 +407,75 @@ public void sendReply(final HttpResponseStatus status, } /** - * Send a file (with zero-copy) to the client. + * Sends the given message as a PNG image. + * This method will block while image is being generated. + * It's only recommended for cases where we want to report an error back to + * the user and the user's browser expects a PNG image. Don't abuse it. + * @param status The status of the request (e.g. 200 OK or 404 Not Found). + * @param msg The message to send as an image. + * @param max_age The expiration time of this entity, in seconds. This is + * not a timestamp, it's how old the resource is allowed to be in the client + * cache. See RFC 2616 section 14.9 for more information. Use 0 to disable + * caching. + */ + public void sendAsPNG(final HttpResponseStatus status, + final String msg, + final int max_age) { + try { + final long now = System.currentTimeMillis() / 1000; + Plot plot = new Plot(now - 1, now); + HashMap params = new HashMap(1); + StringBuilder buf = new StringBuilder(1 + msg.length() + 18); + + buf.append('"'); + escapeJson(msg, buf); + buf.append("\" at graph 0.02,0.97"); + params.put("label", buf.toString()); + buf = null; + plot.setParams(params); + params = null; + final String basepath = + RpcHandler.getDirectoryFromSystemProp("tsd.http.cachedir") + + Integer.toHexString(msg.hashCode()); + GraphHandler.runGnuplot(this, basepath, plot); + plot = null; + sendFile(status, basepath + ".png", max_age); + } catch (Exception e) { + getQueryString().remove("png"); // Avoid recursion. + internalError(new RuntimeException("Failed to generate a PNG with the" + + " following message: " + msg, e)); + } + } + + /** + * Send a file (with zero-copy) to the client with a 200 OK status. * This method doesn't provide any security guarantee. The caller is * responsible for the argument they pass in. * @param path The path to the file to send to the client. * @param max_age The expiration time of this entity, in seconds. This is * not a timestamp, it's how old the resource is allowed to be in the client - * cache. See rfc2616 section 14.9 for more information. Use 0 to disable + * cache. See RFC 2616 section 14.9 for more information. Use 0 to disable * caching. */ public void sendFile(final String path, final int max_age) throws IOException { + sendFile(HttpResponseStatus.OK, path, max_age); + } + + /** + * Send a file (with zero-copy) to the client. + * This method doesn't provide any security guarantee. The caller is + * responsible for the argument they pass in. + * @param status The status of the request (e.g. 200 OK or 404 Not Found). + * @param path The path to the file to send to the client. + * @param max_age The expiration time of this entity, in seconds. This is + * not a timestamp, it's how old the resource is allowed to be in the client + * cache. See RFC 2616 section 14.9 for more information. Use 0 to disable + * caching. + */ + public void sendFile(final HttpResponseStatus status, + final String path, + final int max_age) throws IOException { if (max_age < 0) { throw new IllegalArgumentException("Negative max_age=" + max_age + " for path=" + path); @@ -419,13 +485,16 @@ public void sendFile(final String path, file = new RandomAccessFile(path, "r"); } catch (FileNotFoundException e) { logWarn("File not found: " + e.getMessage()); + if (querystring != null) { + querystring.remove("png"); // Avoid potential recursion. + } notFound(); return; } final long length = file.length(); { final DefaultHttpResponse response = - new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + new DefaultHttpResponse(HttpVersion.HTTP_1_1, status); final String mimetype = guessMimeTypeFromUri(path); response.setHeader(HttpHeaders.Names.CONTENT_TYPE, mimetype == null ? "text/plain" : mimetype);