diff --git a/netwerk/test/httpserver/httpd.js b/netwerk/test/httpserver/httpd.js index 206d01cd928..0083d25855c 100644 --- a/netwerk/test/httpserver/httpd.js +++ b/netwerk/test/httpserver/httpd.js @@ -3332,6 +3332,13 @@ function Response(connection) * to this may be made. */ this._finished = false; + + /** + * True iff powerSeized() has been called on this, signaling that this + * response is to be handled manually by the response handler (which may then + * send arbitrary data in response, even non-HTTP responses). + */ + this._powerSeized = false; } Response.prototype = { @@ -3351,7 +3358,7 @@ Response.prototype = null); this._bodyOutputStream = pipe.outputStream; this._bodyInputStream = pipe.inputStream; - if (this._processAsync) + if (this._processAsync || this._powerSeized) this._startAsyncProcessor(); } @@ -3375,7 +3382,7 @@ Response.prototype = // setStatusLine: function(httpVersion, code, description) { - if (!this._headers || this._finished) + if (!this._headers || this._finished || this._powerSeized) throw Cr.NS_ERROR_NOT_AVAILABLE; this._ensureAlive(); @@ -3420,7 +3427,7 @@ Response.prototype = // setHeader: function(name, value, merge) { - if (!this._headers || this._finished) + if (!this._headers || this._finished || this._powerSeized) throw Cr.NS_ERROR_NOT_AVAILABLE; this._ensureAlive(); @@ -3434,8 +3441,11 @@ Response.prototype = { if (this._finished) throw Cr.NS_ERROR_UNEXPECTED; + if (this._powerSeized) + throw Cr.NS_ERROR_NOT_AVAILABLE; if (this._processAsync) return; + this._ensureAlive(); dumpn("*** processing connection " + this._connection.number + " async"); this._processAsync = true; @@ -3457,23 +3467,60 @@ Response.prototype = this._startAsyncProcessor(); }, + // + // see nsIHttpResponse.seizePower + // + seizePower: function() + { + if (this._processAsync) + throw Cr.NS_ERROR_NOT_AVAILABLE; + if (this._finished) + throw Cr.NS_ERROR_UNEXPECTED; + if (this._powerSeized) + return; + this._ensureAlive(); + + dumpn("*** forcefully seizing power over connection " + + this._connection.number + "..."); + + // Purge any already-written data without sending it. We could as easily + // swap out the streams entirely, but that makes it possible to acquire and + // unknowingly use a stale reference, so we require there only be one of + // each stream ever for any response to avoid this complication. + if (this._asyncCopier) + this._asyncCopier.cancel(Cr.NS_BINDING_ABORTED); + this._asyncCopier = null; + if (this._bodyOutputStream) + { + var input = new BinaryInputStream(this._bodyInputStream); + var avail; + while ((avail = input.available()) > 0) + input.readByteArray(avail); + } + + this._powerSeized = true; + if (this._bodyOutputStream) + this._startAsyncProcessor(); + }, + // // see nsIHttpResponse.finish // finish: function() { - if (!this._processAsync) + if (!this._processAsync && !this._powerSeized) throw Cr.NS_ERROR_UNEXPECTED; if (this._finished) return; - dumpn("*** finishing async connection " + this._connection.number); + dumpn("*** finishing connection " + this._connection.number); this._startAsyncProcessor(); // in case bodyOutputStream was never accessed if (this._bodyOutputStream) this._bodyOutputStream.close(); this._finished = true; }, + // POST-CONSTRUCTION API (not exposed externally) /** @@ -3532,8 +3579,9 @@ Response.prototype = /** * Determines whether this response may be abandoned in favor of a newly - * constructed response, as determined by whether any of this response's data - * has been written to the network. + * constructed response. A response may be abandoned only if it is not being + * sent asynchronously and if raw control over it has not been taken from the + * server. * * @returns boolean * true iff no data has been written to the network @@ -3541,7 +3589,7 @@ Response.prototype = partiallySent: function() { dumpn("*** partiallySent()"); - return this._headers === null; + return this._processAsync || this._powerSeized; }, /** @@ -3551,8 +3599,12 @@ Response.prototype = complete: function() { dumpn("*** complete()"); - if (this._processAsync) + if (this._processAsync || this._powerSeized) + { + NS_ASSERT(this._processAsync ^ this._powerSeized, + "can't both send async and relinquish power"); return; + } NS_ASSERT(!this.partiallySent(), "completing a partially-sent response?"); @@ -3566,9 +3618,11 @@ Response.prototype = /** * Abruptly ends processing of this response, usually due to an error in an * incoming request but potentially due to a bad error handler. Since we - * cannot handle the error in the usual way (giving an HTTP error page in response) - * because data may already have been sent, we stop processing this response - * and abruptly close the connection. + * cannot handle the error in the usual way (giving an HTTP error page in + * response) because data may already have been sent (or because the response + * might be expected to have been generated asynchronously or completely from + * scratch by the handler), we stop processing this response and abruptly + * close the connection. * * @param e : Error * the exception which precipitated this abort, or null if no such exception @@ -3579,11 +3633,34 @@ Response.prototype = dumpn("*** abort(<" + e + ">)"); // This response will be ended by the processor if one was created. - var processor = this._asyncCopier; - if (processor) - processor.cancel(Cr.NS_BINDING_ABORTED); + var copier = this._asyncCopier; + if (copier) + { + // We dispatch asynchronously here so that any pending writes of data to + // the connection will be deterministically written. This makes it easier + // to specify exact behavior, and it makes observable behavior more + // predictable for clients. Note that the correctness of this depends on + // callbacks in response to _waitForData in WriteThroughCopier happening + // asynchronously with respect to the actual writing of data to + // bodyOutputStream, as they currently do; if they happened synchronously, + // an event which ran before this one could write more data to the + // response body before we get around to canceling the copier. We have + // tests for this in test_seizepower.js, however, and I can't think of a + // way to handle both cases without removing bodyOutputStream access and + // moving its effective write(data, length) method onto Response, which + // would be slower and require more code than this anyway. + gThreadManager.currentThread.dispatch({ + run: function() + { + dumpn("*** canceling copy asynchronously..."); + copier.cancel(Cr.NS_ERROR_UNEXPECTED); + } + }, Ci.nsIThreadManager.DISPATCH_NORMAL); + } else + { this.end(); + } }, /** @@ -3616,6 +3693,7 @@ Response.prototype = dumpn("*** _sendHeaders()"); NS_ASSERT(this._headers); + NS_ASSERT(!this._powerSeized); // request-line var statusLine = "HTTP/" + this.httpVersion + " " + @@ -3709,8 +3787,13 @@ Response.prototype = // Send headers if they haven't been sent already. if (this._headers) - this._sendHeaders(); - NS_ASSERT(this._headers === null, "flushHeaders() failed?"); + { + if (this._powerSeized) + this._headers = null; + else + this._sendHeaders(); + NS_ASSERT(this._headers === null, "_sendHeaders() failed?"); + } var response = this; var connection = this._connection; @@ -3732,15 +3815,19 @@ Response.prototype = onStopRequest: function(request, cx, statusCode) { - dumpn("*** onStopRequest [status=" + statusCode.toString(16) + "]"); + dumpn("*** onStopRequest [status=0x" + statusCode.toString(16) + "]"); - if (!Components.isSuccessCode(statusCode)) + if (statusCode === Cr.NS_BINDING_ABORTED) { - dumpn("*** WARNING: non-success statusCode in onStopRequest: " + - statusCode); + dumpn("*** terminating copy observer without ending the response"); } + else + { + if (!Components.isSuccessCode(statusCode)) + dumpn("*** WARNING: non-success statusCode in onStopRequest"); - response.end(); + response.end(); + } }, QueryInterface: function(aIID) @@ -3784,8 +3871,9 @@ function notImplemented() * @param input : nsIAsyncInputStream * the stream from which data is to be read * @param output : nsIOutputStream + * the stream to which data is to be copied * @param observer : nsIRequestObserver - * an observer which will be notified when + * an observer which will be notified when the copy starts and finishes * @param context : nsISupports * context passed to observer when notified of start/stop * @throws NS_ERROR_NULL_POINTER @@ -3847,7 +3935,10 @@ WriteThroughCopier.prototype = dumpn("*** cancel(" + status.toString(16) + ")"); if (this._completed) + { + dumpn("*** ignoring cancel on already-canceled copier..."); return; + } this._completed = true; this.status = status; @@ -3890,13 +3981,16 @@ WriteThroughCopier.prototype = * Receives a more-data-in-input notification and writes the corresponding * data to the output. */ - onInputStreamReady: function() + onInputStreamReady: function(input) { dumpn("*** onInputStreamReady"); if (this._completed) + { + dumpn("*** ignoring stream-ready callback on a canceled copier..."); return; + } - var input = new BinaryInputStream(this._input); + input = new BinaryInputStream(input); try { var avail = input.available(); @@ -3931,6 +4025,19 @@ WriteThroughCopier.prototype = { dumpn("*** _waitForData"); this._input.asyncWait(this, 0, 1, gThreadManager.mainThread); + }, + + /** nsISupports implementation */ + QueryInterface: function(iid) + { + if (iid.equals(Ci.nsIRequest) || + iid.equals(Ci.nsISupports) || + iid.equals(Ci.nsIInputStreamCallback)) + { + return this; + } + + throw Cr.NS_ERROR_NO_INTERFACE; } }; diff --git a/netwerk/test/httpserver/nsIHttpServer.idl b/netwerk/test/httpserver/nsIHttpServer.idl index a7715fc647a..5fd9d54ea2a 100644 --- a/netwerk/test/httpserver/nsIHttpServer.idl +++ b/netwerk/test/httpserver/nsIHttpServer.idl @@ -365,9 +365,17 @@ interface nsIHttpRequestHandler : nsISupports * Processes the HTTP request represented by metadata and initializes the * passed-in response to reflect the correct HTTP response. * - * Note that in some uses of nsIHttpRequestHandler, this method is required to - * not throw an exception; in the general case, however, this method may throw - * an exception (causing an HTTP 500 response to occur). + * If this method throws an exception, externally observable behavior depends + * upon whether is being processed asynchronously and the connection has had + * any data written to it (even an explicit zero bytes of data being written) + * or whether seizePower() has been called on it. If such has happened, sent + * data will be exactly that data written at the time the exception was + * thrown. If no data has been written, the response has not had seizePower() + * called on it, and it is not being asynchronously created, an error handler + * will be invoked (usually 500 unless otherwise specified). Note that some + * uses of nsIHttpRequestHandler may require this method to never throw an + * exception; in the general case, however, this method may throw an exception + * (causing an HTTP 500 response to occur). * * @param metadata * data representing an HTTP request @@ -504,7 +512,8 @@ interface nsIHttpResponse : nsISupports * than 999, or description contains invalid characters * @throws NS_ERROR_NOT_AVAILABLE * if this response is being processed asynchronously and data has been - * written to this response's body + * written to this response's body, or if seizePower() has been called on + * this */ void setStatusLine(in string httpVersion, in unsigned short statusCode, @@ -530,23 +539,29 @@ interface nsIHttpResponse : nsISupports * if name or value is not a valid header component * @throws NS_ERROR_NOT_AVAILABLE * if this response is being processed asynchronously and data has been - * written to this response's body + * written to this response's body, or if seizePower() has been called on + * this */ void setHeader(in string name, in string value, in boolean merge); /** - * A stream to which data appearing in the body of this response should be - * written. After this response has been designated as being processed - * asynchronously, subsequent writes will be synchronously written to the - * underlying transport. However, immediate write-through visible to the HTTP - * client cannot be guaranteed, as intermediate buffers both in the server - * socket and in the client may delay written data; be prepared for potential - * delays. + * A stream to which data appearing in the body of this response (or in the + * totality of the response if seizePower() is called) should be written. + * After this response has been designated as being processed asynchronously, + * or after seizePower() has been called on this, subsequent writes will no + * longer be buffered and will be written to the underlying transport without + * delaying until the entire response is constructed. Write-through may or + * may not be synchronous in the implementation, and in any case particular + * behavior may not be observable to the HTTP client as intermediate buffers + * both in the server socket and in the client may delay written data; be + * prepared for delays at any time. * * @note - * As writes to the underlying transport are synchronous, care must be taken - * not to block on these writes; it is even possible for deadlock to occur - * in the case that the server and the client reside in the same process. + * Although in the asynchronous cases writes to the underlying transport + * are not buffered, care must still be taken not to block for too long on + * any such writes; it is even possible for deadlock to occur in the case + * that the server and the client reside in the same process. Write data in + * small chunks if necessary to avoid this problem. * @throws NS_ERROR_NOT_AVAILABLE * if accessed after this response is fully constructed */ @@ -578,16 +593,43 @@ interface nsIHttpResponse : nsISupports * @throws NS_ERROR_UNEXPECTED * if not initially called within a nsIHttpRequestHandler.handle call or if * called after this response has been finished + * @throws NS_ERROR_NOT_AVAILABLE + * if seizePower() has been called on this */ void processAsync(); + /** + * Seizes complete control of this response (and its connection) from the + * server, allowing raw and unfettered access to data being sent in the HTTP + * response. Once this method has been called the only property which may be + * accessed without an exception being thrown is bodyOutputStream, and the + * only methods which may be accessed without an exception being thrown are + * write(), finish(), and seizePower() (which may be called multiple times + * without ill effect so long as all calls are otherwise allowed). + * + * After a successful call, all data subsequently written to the body of this + * response is written directly to the corresponding connection. (Previously- + * written data is silently discarded.) No status line or headers are sent + * before doing so; if the response handler wishes to write such data, it must + * do so manually. Data generation completes only when finish() is called; it + * is not enough to simply call close() on bodyOutputStream. + * + * @throws NS_ERROR_NOT_AVAILABLE + * if processAsync() has been called on this + * @throws NS_ERROR_UNEXPECTED + * if finish() has been called on this + */ + void seizePower(); + /** * Signals that construction of this response is complete and that it may be - * sent over the network to the client. This method may only be called after - * processAsync() has been called. This method is idempotent. + * sent over the network to the client, or if seizePower() has been called + * signals that all data has been written and that the underlying connection + * may be closed. This method may only be called after processAsync() or + * seizePower() has been called. This method is idempotent. * * @throws NS_ERROR_UNEXPECTED - * if processAsync() has not already been properly called + * if processAsync() or seizePower() has not already been properly called */ void finish(); }; diff --git a/netwerk/test/httpserver/test/test_processasync.js b/netwerk/test/httpserver/test/test_processasync.js index e51fc2cc5a2..b05f6f8e8b4 100644 --- a/netwerk/test/httpserver/test/test_processasync.js +++ b/netwerk/test/httpserver/test/test_processasync.js @@ -299,12 +299,8 @@ function stop_handleAsyncError(ch, cx, status, data) // Lies! But not really! do_check_true(ch.requestSucceeded); - // There's no way server APIs will ever guarantee exactly what data will show - // up here, but they will guarantee sending a (not necessarily strict) prefix - // of what was written. - do_check_true(data.length <= ASYNC_ERROR_BODY.length); - for (var i = 0, sz = data.length; i < sz; i++) - do_check_eq(data[i] == ASYNC_ERROR_BODY.charCodeAt(i)); + do_check_eq(data.length, ASYNC_ERROR_BODY.length); + do_check_eq(String.fromCharCode.apply(null, data), ASYNC_ERROR_BODY); } test = new Test(PREPATH + "/handleAsyncError", diff --git a/netwerk/test/httpserver/test/test_seizepower.js b/netwerk/test/httpserver/test/test_seizepower.js new file mode 100644 index 00000000000..47d08ff52a9 --- /dev/null +++ b/netwerk/test/httpserver/test/test_seizepower.js @@ -0,0 +1,309 @@ +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et: */ +/* ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (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.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is httpd.js code. + * + * The Initial Developer of the Original Code is + * the Mozilla Corporation. + * Portions created by the Initial Developer are Copyright (C) 2009 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Jeff Walden (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK ***** */ + +/* + * Tests that the seizePower API works correctly. + */ + +const PORT = 4444; + +var srv; + +function run_test() +{ + srv = createServer(); + + srv.registerPathHandler("/raw-data", handleRawData); + srv.registerPathHandler("/called-too-late", handleTooLate); + srv.registerPathHandler("/exceptions", handleExceptions); + srv.registerPathHandler("/async-seizure", handleAsyncSeizure); + srv.registerPathHandler("/seize-after-async", handleSeizeAfterAsync); + srv.registerPathHandler("/thrown-exception", handleThrownException); + srv.registerPathHandler("/asap-later-write", handleASAPLaterWrite); + srv.registerPathHandler("/asap-later-finish", handleASAPLaterFinish); + + srv.start(PORT); + + runRawTests(tests, testComplete(srv)); +} + + +function checkException(fun, err, msg) +{ + try + { + fun(); + } + catch (e) + { + if (e !== err && e.result !== err) + do_throw(msg); + return; + } + do_throw(msg); +} + +function callASAPLater(fun) +{ + gThreadManager.currentThread.dispatch({ + run: function() + { + fun(); + } + }, Ci.nsIThreadManager.DISPATCH_NORMAL); +} + + +/***************** + * PATH HANDLERS * + *****************/ + +function handleRawData(request, response) +{ + response.seizePower(); + response.write("Raw data!"); + response.finish(); +} + +function handleTooLate(request, response) +{ + response.write("DO NOT WANT"); + var output = response.bodyOutputStream; + + response.seizePower(); + + if (response.bodyOutputStream !== output) + response.write("bodyOutputStream changed!"); + else + response.write("too-late passed"); + response.finish(); +} + +function handleExceptions(request, response) +{ + response.seizePower(); + checkException(function() { response.setStatusLine("1.0", 500, "ISE"); }, + Cr.NS_ERROR_NOT_AVAILABLE, + "setStatusLine should throw not-available after seizePower"); + checkException(function() { response.setHeader("X-Fail", "FAIL", false); }, + Cr.NS_ERROR_NOT_AVAILABLE, + "setHeader should throw not-available after seizePower"); + checkException(function() { response.processAsync(); }, + Cr.NS_ERROR_NOT_AVAILABLE, + "processAsync should throw not-available after seizePower"); + var out = response.bodyOutputStream; + var data = "exceptions test passed"; + out.write(data, data.length); + response.seizePower(); // idempotency test of seizePower + response.finish(); + response.finish(); // idempotency test of finish after seizePower + checkException(function() { response.seizePower(); }, + Cr.NS_ERROR_UNEXPECTED, + "seizePower should throw unexpected after finish"); +} + +function handleAsyncSeizure(request, response) +{ + response.seizePower(); + callLater(1, function() + { + response.write("async seizure passed"); + response.bodyOutputStream.close(); + callLater(1, function() + { + response.finish(); + }); + }); +} + +function handleSeizeAfterAsync(request, response) +{ + response.setStatusLine(request.httpVersion, 200, "async seizure pass"); + response.processAsync(); + checkException(function() { response.seizePower(); }, + Cr.NS_ERROR_NOT_AVAILABLE, + "seizePower should throw not-available after processAsync"); + callLater(1, function() + { + response.finish(); + }); +} + +function handleThrownException(request, response) +{ + if (request.queryString === "writeBefore") + response.write("ignore this"); + else if (request.queryString === "writeBeforeEmpty") + response.write(""); + else if (request.queryString !== "") + throw "query string FAIL"; + response.seizePower(); + response.write("preparing to throw..."); + throw "badness 10000"; +} + +function handleASAPLaterWrite(request, response) +{ + response.seizePower(); + response.write("should only "); + response.write("see this"); + + callASAPLater(function() + { + response.write("...and not this"); + callASAPLater(function() + { + response.write("...or this"); + response.finish(); + }); + }); + + throw "opening pitch of the ballgame"; +} + +function handleASAPLaterFinish(request, response) +{ + response.seizePower(); + response.write("should only see this"); + + callASAPLater(function() + { + response.finish(); + }); + + throw "out the bum!"; +} + + +/*************** + * BEGIN TESTS * + ***************/ + +var test, data; +var tests = []; + +data = "GET /raw-data HTTP/1.0\r\n" + + "\r\n"; +function checkRawData(data) +{ + do_check_eq(data, "Raw data!"); +} +test = new RawTest("localhost", PORT, data, checkRawData), +tests.push(test); + +data = "GET /called-too-late HTTP/1.0\r\n" + + "\r\n"; +function checkTooLate(data) +{ + do_check_eq(LineIterator(data).next(), "too-late passed"); +} +test = new RawTest("localhost", PORT, data, checkTooLate), +tests.push(test); + +data = "GET /exceptions HTTP/1.0\r\n" + + "\r\n"; +function checkExceptions(data) +{ + do_check_eq("exceptions test passed", data); +} +test = new RawTest("localhost", PORT, data, checkExceptions), +tests.push(test); + +data = "GET /async-seizure HTTP/1.0\r\n" + + "\r\n"; +function checkAsyncSeizure(data) +{ + do_check_eq(data, "async seizure passed"); +} +test = new RawTest("localhost", PORT, data, checkAsyncSeizure), +tests.push(test); + +data = "GET /seize-after-async HTTP/1.0\r\n" + + "\r\n"; +function checkSeizeAfterAsync(data) +{ + do_check_eq(LineIterator(data).next(), "HTTP/1.0 200 async seizure pass"); +} +test = new RawTest("localhost", PORT, data, checkSeizeAfterAsync), +tests.push(test); + +data = "GET /thrown-exception?writeBefore HTTP/1.0\r\n" + + "\r\n"; +function checkThrownExceptionWriteBefore(data) +{ + do_check_eq(data, "preparing to throw..."); +} +test = new RawTest("localhost", PORT, data, checkThrownExceptionWriteBefore), +tests.push(test); + +data = "GET /thrown-exception?writeBeforeEmpty HTTP/1.0\r\n" + + "\r\n"; +function checkThrownExceptionWriteBefore(data) +{ + do_check_eq(data, "preparing to throw..."); +} +test = new RawTest("localhost", PORT, data, checkThrownExceptionWriteBefore), +tests.push(test); + +data = "GET /thrown-exception HTTP/1.0\r\n" + + "\r\n"; +function checkThrownException(data) +{ + do_check_eq(data, "preparing to throw..."); +} +test = new RawTest("localhost", PORT, data, checkThrownException), +tests.push(test); + +data = "GET /asap-later-write HTTP/1.0\r\n" + + "\r\n"; +function checkASAPLaterWrite(data) +{ + do_check_eq(data, "should only see this"); +} +test = new RawTest("localhost", PORT, data, checkASAPLaterWrite), +tests.push(test); + +data = "GET /asap-later-finish HTTP/1.0\r\n" + + "\r\n"; +function checkASAPLaterFinish(data) +{ + do_check_eq(data, "should only see this"); +} +test = new RawTest("localhost", PORT, data, checkASAPLaterFinish), +tests.push(test);