diff options
| author | John Christopher Anderson <jchris@apache.org> | 2009-04-18 20:15:44 +0000 | 
|---|---|---|
| committer | John Christopher Anderson <jchris@apache.org> | 2009-04-18 20:15:44 +0000 | 
| commit | 3e47bfd6586f42f9fe8e49cea03c4df976c781a1 (patch) | |
| tree | b3ae7a8203f2d4ec99a84762ea9aedda56864d88 | |
| parent | daa9d65a53dedaac5aeeb6394d4c0b6f99fa930c (diff) | |
refactor main.js into many files and improve show/list error handling
git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@766383 13f79535-47bb-0310-9956-ffa450edef68
| -rw-r--r-- | share/server/loop.js | 62 | ||||
| -rw-r--r-- | share/server/main.js | 600 | ||||
| -rwxr-xr-x | share/server/mainjs.sh | 24 | ||||
| -rw-r--r-- | share/server/render.js | 252 | ||||
| -rw-r--r-- | share/server/state.js | 32 | ||||
| -rw-r--r-- | share/server/util.js | 112 | ||||
| -rw-r--r-- | share/server/validate.js | 23 | ||||
| -rw-r--r-- | share/server/views.js | 117 | ||||
| -rw-r--r-- | share/www/script/test/list_views.js | 12 | ||||
| -rw-r--r-- | share/www/script/test/show_documents.js | 7 | ||||
| -rw-r--r-- | src/couchdb/couch_httpd.erl | 26 | ||||
| -rw-r--r-- | src/couchdb/couch_httpd_show.erl | 83 | ||||
| -rw-r--r-- | src/couchdb/couch_httpd_view.erl | 26 | 
13 files changed, 1046 insertions, 330 deletions
| diff --git a/share/server/loop.js b/share/server/loop.js new file mode 100644 index 00000000..170a8dc8 --- /dev/null +++ b/share/server/loop.js @@ -0,0 +1,62 @@ +// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the +// License for the specific language governing permissions and limitations under +// the License. + +var sandbox = null; + +try { +  // if possible, use evalcx (not always available) +  sandbox = evalcx(''); +  sandbox.emit = emit; +  sandbox.sum = sum; +  sandbox.log = log; +  sandbox.toJSON = toJSON; +  sandbox.respondWith = respondWith; +  sandbox.registerType = registerType; +} catch (e) {} + +// Commands are in the form of json arrays: +// ["commandname",..optional args...]\n +// +// Responses are json values followed by a new line ("\n") + +var cmd, cmdkey; + +var dispatch = { +  "reset"      : State.reset, +  "add_fun"    : State.addFun, +  "map_doc"    : Views.mapDoc, +  "reduce"     : Views.reduce, +  "rereduce"   : Views.rereduce, +  "validate"   : Validate.validate, +  "show_doc"   : Render.showDoc, +  "list_begin" : Render.listBegin, +  "list_row"   : Render.listRow, +  "list_tail"  : Render.listTail  +}; + +while (cmd = eval(readline())) { +  try { +    cmdkey = cmd.shift(); +    if (dispatch[cmdkey]) { +      // run the correct responder with the cmd body +      dispatch[cmdkey].apply(this, cmd); +    } else { +      // unknown command, quit and hope the restarted version is better +      respond({ +        error: "query_server_error", +        reason: "unknown command '" + cmdkey + "'"}); +      quit(); +    } +  } catch(e) { +    respond(e); +  } +}; diff --git a/share/server/main.js b/share/server/main.js index 3c59892b..4f57fa6b 100644 --- a/share/server/main.js +++ b/share/server/main.js @@ -10,33 +10,6 @@  // License for the specific language governing permissions and limitations under  // the License. -var cmd; -var funs = [];        // holds functions used for computation -var map_results = []; // holds temporary emitted values during doc map -var row_line = {}; // holds row number in list per func - -var sandbox = null; - -emit = function(key, value) { -  map_results.push([key, value]); -} - -sum = function(values) { -  var rv = 0; -  for (var i in values) { -    rv += values[i]; -  } -  return rv; -} - -log = function(message) { -  if (typeof message == "undefined") { -    message = "Error: attempting to log message of 'undefined'."; -  } else if (typeof message != "string") { -    message = toJSON(message); -  } -  print(toJSON({log: message})); -}  // mimeparse.js  // http://code.google.com/p/mimeparse/ @@ -46,12 +19,12 @@ log = function(message) {  var Mimeparse = (function() {    function strip(string) { -    return string.replace(/^\s+/, '').replace(/\s+$/, '') +    return string.replace(/^\s+/, '').replace(/\s+$/, '');    };    function parseRanges(ranges) {      var parsedRanges = [], rangeParts = ranges.split(",");      for (var i=0; i < rangeParts.length; i++) { -      parsedRanges.push(publicMethods.parseMediaRange(rangeParts[i])) +      parsedRanges.push(publicMethods.parseMediaRange(rangeParts[i]));      };      return parsedRanges;    }; @@ -130,12 +103,12 @@ var Mimeparse = (function() {        var parsedHeader = parseRanges(header);        var weighted = [];        for (var i=0; i < supported.length; i++) { -        weighted.push([publicMethods.fitnessAndQualityParsed(supported[i], parsedHeader), supported[i]]) +        weighted.push([publicMethods.fitnessAndQualityParsed(supported[i], parsedHeader), supported[i]]);        };        weighted.sort();        return weighted[weighted.length-1][0][1] ? weighted[weighted.length-1][1] : '';      } -  } +  };    return publicMethods;  })(); @@ -163,7 +136,7 @@ respondWith = function(req, responders) {    } else {      throw({code:406, body:"Not Acceptable: "+accept});        } -} +};  // whoever registers last wins.  mimesByKey = {}; @@ -203,204 +176,33 @@ registerType("yaml", "application/x-yaml", "text/yaml");  registerType("multipart_form", "multipart/form-data");  registerType("url_encoded_form", "application/x-www-form-urlencoded"); -// ok back to business. -try { -  // if possible, use evalcx (not always available) -  sandbox = evalcx(''); -  sandbox.emit = emit; -  sandbox.sum = sum; -  sandbox.log = log; -  sandbox.toJSON = toJSON; -  sandbox.respondWith = respondWith; -  sandbox.registerType = registerType; -} catch (e) {} - -// Commands are in the form of json arrays: -// ["commandname",..optional args...]\n -// -// Responses are json values followed by a new line ("\n") - -while (cmd = eval(readline())) { -  try { -    switch (cmd[0]) { -      case "reset": -        // clear the globals and run gc -        funs = []; -        gc(); -        print("true"); // indicates success -        break; -      case "add_fun": -        // The second arg is a string that will compile to a function. -        // and then we add it to funs array -        funs.push(compileFunction(cmd[1])); -        print("true"); -        break; -      case "map_doc": -        // The second arg is a document. We compute all the map functions against -        // it. -        // -        // Each function can output multiple keys value, pairs for each document -        // -        // Example output of map_doc after three functions set by add_fun cmds: -        // [ -        //  [["Key","Value"]],                    <- fun 1 returned 1 key value -        //  [],                                   <- fun 2 returned 0 key values -        //  [["Key1","Value1"],["Key2","Value2"]] <- fun 3 returned 2 key values -        // ] -        // -        var doc = cmd[1]; -        /* -        Immutable document support temporarily removed. -         -        Removed because the seal function no longer works on JS 1.8 arrays, -        instead returning an error. The sealing is meant to prevent map -        functions from modifying the same document that is passed to other map -        functions. However, only map functions in the same design document are -        run together, so we have a reasonable expectation they can trust each -        other. Any map fun that can't be trusted can be placed in its own -        design document, and it cannot affect other map functions. -         -        recursivelySeal(doc); // seal to prevent map functions from changing doc -        */ -        var buf = []; -        for (var i = 0; i < funs.length; i++) { -          map_results = []; -          try { -            funs[i](doc); -            buf.push(toJSON(map_results)); -          } catch (err) { -            if (err == "fatal_error") { -              // Only if it's a "fatal_error" do we exit. What's a fatal error? -              // That's for the query to decide. -              // -              // This will make it possible for queries to completely error out, -              // by catching their own local exception and rethrowing a -              // fatal_error. But by default if they don't do error handling we -              // just eat the exception and carry on. -              throw {error: "map_runtime_error", -                  reason: "function raised fatal exception"}; -            } -            print(toJSON({log: "function raised exception (" + err  -              + ") with doc._id " + doc._id})); -            buf.push("[]"); -          } -        } -        print("[" + buf.join(", ") + "]"); -        break; - -      case "rereduce": -      case "reduce": -        { -        var keys = null; -        var values = null; -        var reduceFuns = cmd[1]; -        var rereduce = false; -         -        if (cmd[0] == "reduce") { -          var kvs = cmd[2]; -          keys = new Array(kvs.length); -          values = new Array(kvs.length); -          for(var i = 0; i < kvs.length; i++) { -              keys[i] = kvs[i][0]; -              values[i] = kvs[i][1]; -          } -        } else { -          values = cmd[2]; -          rereduce = true; -        } - -        for (var i in reduceFuns) { -          reduceFuns[i] = compileFunction(reduceFuns[i]); -        } - -        var reductions = new Array(funs.length); -        for(var i = 0; i < reduceFuns.length; i++) { -          try { -            reductions[i] = reduceFuns[i](keys, values, rereduce); -          } catch (err) { -            if (err == "fatal_error") { -              throw {error: "reduce_runtime_error", -                  reason: "function raised fatal exception"}; -            } -            print(toJSON({log: "function raised exception (" + err + ")"})); -            reductions[i] = null; -          } -        } -        print("[true," + toJSON(reductions) + "]"); -        } -        break; -      case "validate": -        var funSrc = cmd[1]; -        var newDoc = cmd[2]; -        var oldDoc = cmd[3]; -        var userCtx = cmd[4]; -        var validateFun = compileFunction(funSrc); -        try { -          validateFun(newDoc, oldDoc, userCtx); -          print("1"); -        } catch (error) { -          respond(error); -        } -        break; -      case "show_doc": -        var funSrc = cmd[1]; -        var doc = cmd[2]; -        var req = cmd[3]; -        var formFun = compileFunction(funSrc); -        runRenderFunction(formFun, [doc, req]); -        break; -      case "list_begin": -        var listFun = funs[0]; -        var head = cmd[1]; -        var req = cmd[2]; -        row_line[listFun] = { first_key: null, row_number: 0, prev_key: null }; -        runRenderFunction(listFun, [head, null, req, null]); -        break; -      case "list_row": -        var listFun = funs[0]; -        var row = cmd[1]; -        var req = cmd[2]; -        var row_info = row_line[listFun]; -        runRenderFunction(listFun, [null, row, req, row_info]); -        if (row_info.first_key == null) { -          row_info.first_key = row.key; -        } -        row_info.prev_key = row.key; -        row_info.row_number++; -        row_line[listFun] = row_info; -        break; -      case "list_tail": -        var listFun = funs[0]; -        var req = cmd[1]; -        var row_info = null; -        try { -            row_info = row_line[listFun]; -            delete row_line[listFun]; -        } catch (e) {} -        runRenderFunction(listFun, [null, null, req, row_info]); -        break; -      default: -        print(toJSON({error: "query_server_error", -            reason: "unknown command '" + cmd[0] + "'"})); -        quit(); +var Render = (function() { +  var row_info; +  return { +    showDoc : function(funSrc, doc, req) { +      var formFun = compileFunction(funSrc); +      runRenderFunction(formFun, [doc, req], funSrc); +    }, +    listBegin : function(head, req) { +      row_info = { first_key: null, row_number: 0, prev_key: null }; +      runRenderFunction(funs[0], [head, null, req, null], funsrc[0]); +    }, +    listRow : function(row, req) { +      if (row_info.first_key == null) { +        row_info.first_key = row.key; +      } +      runRenderFunction(funs[0], [null, row, req, row_info], funsrc[0], true); +      row_info.prev_key = row.key; +      row_info.row_number++; +    }, +    listTail : function(req) { +      runRenderFunction(funs[0], [null, null, req, row_info], funsrc[0]);      } -  } catch (exception) { -    print(toJSON(exception));    } -} - -function maybeWrapResponse(resp) { -  var type = typeof resp; -  if ((type == "string") || (type == "xml")) { -    return {body:resp}; -  } else { -    return resp; -  } -}; +})(); -var responseSent; -function runRenderFunction(renderFun, args) { +function runRenderFunction(renderFun, args, funSrc, htmlErrors) {    responseSent = false;    try {      var resp = renderFun.apply(null, args); @@ -412,46 +214,85 @@ function runRenderFunction(renderFun, args) {        }            }    } catch(e) { -    log("function raised error: "+e.toString()); -    log("stacktrace: "+e.stack); -    var errorMessage = "JavaScript function raised error: "+e.toString()+"\nSee CouchDB logfile for stacktrace."; -    respond({error:"render_error",reason:errorMessage}); +    var logMessage = "function raised error: "+e.toString(); +    log(logMessage); +    // log("stacktrace: "+e.stack); +    var errorMessage = htmlErrors ? htmlRenderError(e, funSrc) : logMessage; +    respond({ +      error:"render_error", +      reason:errorMessage});    }  }; -// prints the object as JSON, and rescues and logs any toJSON() related errors -function respond(obj) { -  responseSent = true; -  try { -    print(toJSON(obj));   -  } catch(e) { -    log("Error converting object to JSON: " + e.toString()); -  } +function escapeHTML(string) { +  return string.replace(/&/g, "&") +               .replace(/</g, "<") +               .replace(/>/g, ">");  } -function compileFunction(source) { -  try { -    var functionObject = sandbox ? evalcx(source, sandbox) : eval(source); -  } catch (err) { -    throw {error: "compilation_error", -      reason: err.toString() + " (" + source + ")"}; -  } -  if (typeof(functionObject) == "function") { -    return functionObject; +function htmlRenderError(e, funSrc) { +  var msg = ["<html><body><h1>Render Error</h1>", +    "<p>JavaScript function raised error: ", +    e.toString(), +    "</p><h2>Stacktrace:</h2><code><pre>", +    escapeHTML(e.stack), +    "</pre></code><h2>Function source:</h2><code><pre>", +    escapeHTML(funSrc), +    "</pre></code></body></html>"].join(''); +  return {body:msg}; +}; + +function maybeWrapResponse(resp) { +  var type = typeof resp; +  if ((type == "string") || (type == "xml")) { +    return {body:resp};    } else { -    throw {error: "compilation_error", -      reason: "expression does not eval to a function. (" + source + ")"}; +    return resp;    } -} +}; +// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the +// License for the specific language governing permissions and limitations under +// the License. -function recursivelySeal(obj) { -  seal(obj); -  for (var propname in obj) { -    if (typeof doc[propname] == "object") { -      recursivelySeal(doc[propname]); +// globals used by other modules and functions +var funs = [];        // holds functions used for computation +var funsrc = [];      // holds function source for debug info +var State = (function() { +  return { +    reset : function() { +      // clear the globals and run gc +      funs = []; +      funsrc = []; +      gc(); +      print("true"); // indicates success +    }, +    addFun : function(newFun) { +      // Compile to a function and add it to funs array +      funsrc.push(newFun); +      funs.push(compileFunction(newFun)); +      print("true");      }    } -} +})(); +// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the +// License for the specific language governing permissions and limitations under +// the License.  function toJSON(val) {    if (typeof(val) == "undefined") { @@ -474,7 +315,7 @@ function toJSON(val) {        return v.toString();      },      "Date": function(v) { -      var f = function(n) { return n < 10 ? '0' + n : n } +      var f = function(n) { return n < 10 ? '0' + n : n };        return '"' + v.getUTCFullYear()   + '-' +                   f(v.getUTCMonth() + 1) + '-' +                   f(v.getUTCDate())      + 'T' + @@ -509,3 +350,248 @@ function toJSON(val) {      }    }[val != null ? val.constructor.name : "Object"](val);  } + +function compileFunction(source) { +  try { +    var functionObject = sandbox ? evalcx(source, sandbox) : eval(source); +  } catch (err) { +    throw {error: "compilation_error", +      reason: err.toString() + " (" + source + ")"}; +  } +  if (typeof(functionObject) == "function") { +    return functionObject; +  } else { +    throw {error: "compilation_error", +      reason: "expression does not eval to a function. (" + source + ")"}; +  } +} + +function recursivelySeal(obj) { +  seal(obj); +  for (var propname in obj) { +    if (typeof doc[propname] == "object") { +      recursivelySeal(doc[propname]); +    } +  } +} + +var responseSent; +// prints the object as JSON, and rescues and logs any toJSON() related errors +function respond(obj) { +  responseSent = true; +  try { +    print(toJSON(obj));   +  } catch(e) { +    log("Error converting object to JSON: " + e.toString()); +  } +}; + +log = function(message) { +  if (typeof message == "undefined") { +    message = "Error: attempting to log message of 'undefined'."; +  } else if (typeof message != "string") { +    message = toJSON(message); +  } +  print(toJSON({log: message})); +}; +// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the +// License for the specific language governing permissions and limitations under +// the License. + +var Validate = { +  validate : function(funSrc, newDoc, oldDoc, userCtx) { +    var validateFun = compileFunction(funSrc); +    try { +      validateFun(newDoc, oldDoc, userCtx); +      print("1"); +    } catch (error) { +      respond(error); +    } +  } +};// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the +// License for the specific language governing permissions and limitations under +// the License. + +// globals used by views +var map_results = []; // holds temporary emitted values during doc map + +// view helper functions +emit = function(key, value) { +  map_results.push([key, value]); +} + +sum = function(values) { +  var rv = 0; +  for (var i in values) { +    rv += values[i]; +  } +  return rv; +} + +var Views = (function() { +     +  function runReduce(reduceFuns, keys, values, rereduce) { +    for (var i in reduceFuns) { +      reduceFuns[i] = compileFunction(reduceFuns[i]); +    } +    var reductions = new Array(reduceFuns.length); +    for(var i = 0; i < reduceFuns.length; i++) { +      try { +        reductions[i] = reduceFuns[i](keys, values, rereduce); +      } catch (err) { +        if (err == "fatal_error") { +          throw { +            error: "reduce_runtime_error", +            reason: "function raised fatal exception"}; +        } +        log("function raised exception (" + err + ")"); +        reductions[i] = null; +      } +    } +    print("[true," + toJSON(reductions) + "]"); +  }; +   +  return { +    reduce : function(reduceFuns, kvs) { +      var keys = new Array(kvs.length); +      var values = new Array(kvs.length); +      for(var i = 0; i < kvs.length; i++) { +          keys[i] = kvs[i][0]; +          values[i] = kvs[i][1]; +      } +      runReduce(reduceFuns, keys, values, false); +    }, +    rereduce : function(reduceFuns, values) { +      runReduce(reduceFuns, null, values, true); +    }, +    mapDoc : function(doc) { +      // Compute all the map functions against the document. +      // +      // Each function can output multiple key/value pairs for each document. +      // +      // Example output of map_doc after three functions set by add_fun cmds: +      // [ +      //  [["Key","Value"]],                    <- fun 1 returned 1 key value +      //  [],                                   <- fun 2 returned 0 key values +      //  [["Key1","Value1"],["Key2","Value2"]] <- fun 3 returned 2 key values +      // ] +      // + +      /* +      Immutable document support temporarily removed. + +      Removed because the seal function no longer works on JS 1.8 arrays, +      instead returning an error. The sealing is meant to prevent map +      functions from modifying the same document that is passed to other map +      functions. However, only map functions in the same design document are +      run together, so we have a reasonable expectation they can trust each +      other. Any map fun that can't be trusted can be placed in its own +      design document, and it cannot affect other map functions. + +      recursivelySeal(doc); // seal to prevent map functions from changing doc +      */ +      var buf = []; +      for (var i = 0; i < funs.length; i++) { +        map_results = []; +        try { +          funs[i](doc); +          buf.push(toJSON(map_results)); +        } catch (err) { +          if (err == "fatal_error") { +            // Only if it's a "fatal_error" do we exit. What's a fatal error? +            // That's for the query to decide. +            // +            // This will make it possible for queries to completely error out, +            // by catching their own local exception and rethrowing a +            // fatal_error. But by default if they don't do error handling we +            // just eat the exception and carry on. +            throw { +              error: "map_runtime_error", +              reason: "function raised fatal exception"}; +          } +          log("function raised exception (" + err + ") with doc._id " + doc._id); +          buf.push("[]"); +        } +      } +      print("[" + buf.join(", ") + "]"); +    } +  } +})(); +// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the +// License for the specific language governing permissions and limitations under +// the License. + +var sandbox = null; + +try { +  // if possible, use evalcx (not always available) +  sandbox = evalcx(''); +  sandbox.emit = emit; +  sandbox.sum = sum; +  sandbox.log = log; +  sandbox.toJSON = toJSON; +  sandbox.respondWith = respondWith; +  sandbox.registerType = registerType; +} catch (e) {} + +// Commands are in the form of json arrays: +// ["commandname",..optional args...]\n +// +// Responses are json values followed by a new line ("\n") + +var cmd, cmdkey; + +var dispatch = { +  "reset"      : State.reset, +  "add_fun"    : State.addFun, +  "map_doc"    : Views.mapDoc, +  "reduce"     : Views.reduce, +  "rereduce"   : Views.rereduce, +  "validate"   : Validate.validate, +  "show_doc"   : Render.showDoc, +  "list_begin" : Render.listBegin, +  "list_row"   : Render.listRow, +  "list_tail"  : Render.listTail  +}; + +while (cmd = eval(readline())) { +  try { +    cmdkey = cmd.shift(); +    if (dispatch[cmdkey]) { +      // run the correct responder with the cmd body +      dispatch[cmdkey].apply(this, cmd); +    } else { +      // unknown command, quit and hope the restarted version is better +      respond({ +        error: "query_server_error", +        reason: "unknown command '" + cmdkey + "'"}); +      quit(); +    } +  } catch(e) { +    respond(e); +  } +}; diff --git a/share/server/mainjs.sh b/share/server/mainjs.sh new file mode 100755 index 00000000..86128423 --- /dev/null +++ b/share/server/mainjs.sh @@ -0,0 +1,24 @@ +#! /bin/sh -e + +# Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the +# License for the specific language governing permissions and limitations under +# the License. + +dirname=`dirname $0` + +cat \ +  $dirname/render.js \ +  $dirname/state.js \ +  $dirname/util.js \ +  $dirname/validate.js \ +  $dirname/views.js \ +  $dirname/loop.js \ +  > $dirname/main.js diff --git a/share/server/render.js b/share/server/render.js new file mode 100644 index 00000000..89c31ea1 --- /dev/null +++ b/share/server/render.js @@ -0,0 +1,252 @@ +// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the +// License for the specific language governing permissions and limitations under +// the License. + + +// mimeparse.js +// http://code.google.com/p/mimeparse/ +// Code with comments: http://mimeparse.googlecode.com/svn/trunk/mimeparse.js +// Tests: http://mimeparse.googlecode.com/svn/trunk/mimeparse-js-test.html +// Ported from version 0.1.2 + +var Mimeparse = (function() { +  function strip(string) { +    return string.replace(/^\s+/, '').replace(/\s+$/, ''); +  }; +  function parseRanges(ranges) { +    var parsedRanges = [], rangeParts = ranges.split(","); +    for (var i=0; i < rangeParts.length; i++) { +      parsedRanges.push(publicMethods.parseMediaRange(rangeParts[i])); +    }; +    return parsedRanges; +  }; +  var publicMethods = { +    parseMimeType : function(mimeType) { +      var fullType, typeParts, params = {}, parts = mimeType.split(';'); +      for (var i=0; i < parts.length; i++) { +        var p = parts[i].split('='); +        if (p.length == 2) { +          params[strip(p[0])] = strip(p[1]); +        } +      }; +      fullType = parts[0].replace(/^\s+/, '').replace(/\s+$/, ''); +      if (fullType == '*') fullType = '*/*'; +      typeParts = fullType.split('/'); +      return [typeParts[0], typeParts[1], params]; +    }, +    parseMediaRange : function(range) { +      var q, parsedType = this.parseMimeType(range); +      if (!parsedType[2]['q']) { +        parsedType[2]['q'] = '1'; +      } else { +        q = parseFloat(parsedType[2]['q']); +        if (isNaN(q)) { +          parsedType[2]['q'] = '1'; +        } else if (q > 1 || q < 0) { +          parsedType[2]['q'] = '1'; +        } +      } +      return parsedType; +    }, +    fitnessAndQualityParsed : function(mimeType, parsedRanges) { +      var bestFitness = -1, bestFitQ = 0, target = this.parseMediaRange(mimeType); +      var targetType = target[0], targetSubtype = target[1], targetParams = target[2]; + +      for (var i=0; i < parsedRanges.length; i++) { +        var parsed = parsedRanges[i]; +        var type = parsed[0], subtype = parsed[1], params = parsed[2]; +        if ((type == targetType || type == "*" || targetType == "*") && +          (subtype == targetSubtype || subtype == "*" || targetSubtype == "*")) { +          var matchCount = 0; +          for (param in targetParams) { +            if (param != 'q' && params[param] && params[param] == targetParams[param]) { +              matchCount += 1; +            } +          } + +          var fitness = (type == targetType) ? 100 : 0; +          fitness += (subtype == targetSubtype) ? 10 : 0; +          fitness += matchCount; + +          if (fitness > bestFitness) { +            bestFitness = fitness; +            bestFitQ = params["q"]; +          } +        } +      }; +      return [bestFitness, parseFloat(bestFitQ)]; +    }, +    qualityParsed : function(mimeType, parsedRanges) { +      return this.fitnessAndQualityParsed(mimeType, parsedRanges)[1]; +    }, +    quality : function(mimeType, ranges) { +      return this.qualityParsed(mimeType, parseRanges(ranges)); +    }, + +    // Takes a list of supported mime-types and finds the best +    // match for all the media-ranges listed in header. The value of +    // header must be a string that conforms to the format of the +    // HTTP Accept: header. The value of 'supported' is a list of +    // mime-types. +    // +    // >>> bestMatch(['application/xbel+xml', 'text/xml'], 'text/*;q=0.5,*/*; q=0.1') +    // 'text/xml' +    bestMatch : function(supported, header) { +      var parsedHeader = parseRanges(header); +      var weighted = []; +      for (var i=0; i < supported.length; i++) { +        weighted.push([publicMethods.fitnessAndQualityParsed(supported[i], parsedHeader), supported[i]]); +      }; +      weighted.sort(); +      return weighted[weighted.length-1][0][1] ? weighted[weighted.length-1][1] : ''; +    } +  }; +  return publicMethods; +})(); + +// this function provides a shortcut for managing responses by Accept header +respondWith = function(req, responders) { +  var bestKey = null, accept = req.headers["Accept"]; +  if (accept && !req.query.format) { +    var provides = []; +    for (key in responders) { +      if (mimesByKey[key]) { +        provides = provides.concat(mimesByKey[key]);         +      } +    } +    var bestMime = Mimeparse.bestMatch(provides, accept); +    bestKey = keysByMime[bestMime]; +  } else { +    bestKey = req.query.format; +  } +  var rFunc = responders[bestKey || responders.fallback || "html"]; +  if (rFunc) {       +    var resp = maybeWrapResponse(rFunc()); +    resp["headers"] = resp["headers"] || {}; +    resp["headers"]["Content-Type"] = bestMime; +    respond(resp); +  } else { +    throw({code:406, body:"Not Acceptable: "+accept});     +  } +}; + +// whoever registers last wins. +mimesByKey = {}; +keysByMime = {}; +registerType = function() { +  var mimes = [], key = arguments[0]; +  for (var i=1; i < arguments.length; i++) { +    mimes.push(arguments[i]); +  }; +  mimesByKey[key] = mimes; +  for (var i=0; i < mimes.length; i++) { +    keysByMime[mimes[i]] = key; +  }; +}; + +// Some default types +// Ported from Ruby on Rails +// Build list of Mime types for HTTP responses +// http://www.iana.org/assignments/media-types/ +// http://dev.rubyonrails.org/svn/rails/trunk/actionpack/lib/action_controller/mime_types.rb + +registerType("all", "*/*"); +registerType("text", "text/plain", "txt"); +registerType("html", "text/html"); +registerType("xhtml", "application/xhtml+xml", "xhtml"); +registerType("xml", "application/xml", "text/xml", "application/x-xml"); +// http://www.ietf.org/rfc/rfc4627.txt +registerType("json", "application/json", "text/x-json"); +registerType("js", "text/javascript", "application/javascript", "application/x-javascript"); +registerType("css", "text/css"); +registerType("ics", "text/calendar"); +registerType("csv", "text/csv"); +registerType("rss", "application/rss+xml"); +registerType("atom", "application/atom+xml"); +registerType("yaml", "application/x-yaml", "text/yaml"); +// just like Rails +registerType("multipart_form", "multipart/form-data"); +registerType("url_encoded_form", "application/x-www-form-urlencoded"); + + +var Render = (function() { +  var row_info; +  return { +    showDoc : function(funSrc, doc, req) { +      var formFun = compileFunction(funSrc); +      runRenderFunction(formFun, [doc, req], funSrc); +    }, +    listBegin : function(head, req) { +      row_info = { first_key: null, row_number: 0, prev_key: null }; +      runRenderFunction(funs[0], [head, null, req, null], funsrc[0]); +    }, +    listRow : function(row, req) { +      if (row_info.first_key == null) { +        row_info.first_key = row.key; +      } +      runRenderFunction(funs[0], [null, row, req, row_info], funsrc[0], true); +      row_info.prev_key = row.key; +      row_info.row_number++; +    }, +    listTail : function(req) { +      runRenderFunction(funs[0], [null, null, req, row_info], funsrc[0]); +    } +  } +})(); + +function runRenderFunction(renderFun, args, funSrc, htmlErrors) { +  responseSent = false; +  try { +    var resp = renderFun.apply(null, args); +    if (!responseSent) { +      if (resp) { +        respond(maybeWrapResponse(resp));        +      } else { +        respond({error:"render_error",reason:"undefined response from render function"}); +      }       +    } +  } catch(e) { +    var logMessage = "function raised error: "+e.toString(); +    log(logMessage); +    // log("stacktrace: "+e.stack); +    var errorMessage = htmlErrors ? htmlRenderError(e, funSrc) : logMessage; +    respond({ +      error:"render_error", +      reason:errorMessage}); +  } +}; + +function escapeHTML(string) { +  return string.replace(/&/g, "&") +               .replace(/</g, "<") +               .replace(/>/g, ">"); +} + +function htmlRenderError(e, funSrc) { +  var msg = ["<html><body><h1>Render Error</h1>", +    "<p>JavaScript function raised error: ", +    e.toString(), +    "</p><h2>Stacktrace:</h2><code><pre>", +    escapeHTML(e.stack), +    "</pre></code><h2>Function source:</h2><code><pre>", +    escapeHTML(funSrc), +    "</pre></code></body></html>"].join(''); +  return {body:msg}; +}; + +function maybeWrapResponse(resp) { +  var type = typeof resp; +  if ((type == "string") || (type == "xml")) { +    return {body:resp}; +  } else { +    return resp; +  } +}; diff --git a/share/server/state.js b/share/server/state.js new file mode 100644 index 00000000..05c55a1b --- /dev/null +++ b/share/server/state.js @@ -0,0 +1,32 @@ +// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the +// License for the specific language governing permissions and limitations under +// the License. + +// globals used by other modules and functions +var funs = [];        // holds functions used for computation +var funsrc = [];      // holds function source for debug info +var State = (function() { +  return { +    reset : function() { +      // clear the globals and run gc +      funs = []; +      funsrc = []; +      gc(); +      print("true"); // indicates success +    }, +    addFun : function(newFun) { +      // Compile to a function and add it to funs array +      funsrc.push(newFun); +      funs.push(compileFunction(newFun)); +      print("true"); +    } +  } +})(); diff --git a/share/server/util.js b/share/server/util.js new file mode 100644 index 00000000..7faf2f0b --- /dev/null +++ b/share/server/util.js @@ -0,0 +1,112 @@ +// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the +// License for the specific language governing permissions and limitations under +// the License. + +function toJSON(val) { +  if (typeof(val) == "undefined") { +    throw "Cannot encode 'undefined' value as JSON"; +  } +  var subs = {'\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', +              '\r': '\\r', '"' : '\\"', '\\': '\\\\'}; +  if (typeof(val) == "xml") { // E4X support +    val = val.toXMLString(); +  } +  return { +    "Array": function(v) { +      var buf = []; +      for (var i = 0; i < v.length; i++) { +        buf.push(toJSON(v[i])); +      } +      return "[" + buf.join(",") + "]"; +    }, +    "Boolean": function(v) { +      return v.toString(); +    }, +    "Date": function(v) { +      var f = function(n) { return n < 10 ? '0' + n : n }; +      return '"' + v.getUTCFullYear()   + '-' + +                 f(v.getUTCMonth() + 1) + '-' + +                 f(v.getUTCDate())      + 'T' + +                 f(v.getUTCHours())     + ':' + +                 f(v.getUTCMinutes())   + ':' + +                 f(v.getUTCSeconds())   + 'Z"'; +    }, +    "Number": function(v) { +      return isFinite(v) ? v.toString() : "null"; +    }, +    "Object": function(v) { +      if (v === null) return "null"; +      var buf = []; +      for (var k in v) { +        if (!v.hasOwnProperty(k) || typeof(k) !== "string" || v[k] === undefined) { +          continue; +        } +        buf.push(toJSON(k, val) + ": " + toJSON(v[k])); +      } +      return "{" + buf.join(",") + "}"; +    }, +    "String": function(v) { +      if (/["\\\x00-\x1f]/.test(v)) { +        v = v.replace(/([\x00-\x1f\\"])/g, function(a, b) { +          var c = subs[b]; +          if (c) return c; +          c = b.charCodeAt(); +          return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16); +        }); +      } +      return '"' + v + '"'; +    } +  }[val != null ? val.constructor.name : "Object"](val); +} + +function compileFunction(source) { +  try { +    var functionObject = sandbox ? evalcx(source, sandbox) : eval(source); +  } catch (err) { +    throw {error: "compilation_error", +      reason: err.toString() + " (" + source + ")"}; +  } +  if (typeof(functionObject) == "function") { +    return functionObject; +  } else { +    throw {error: "compilation_error", +      reason: "expression does not eval to a function. (" + source + ")"}; +  } +} + +function recursivelySeal(obj) { +  seal(obj); +  for (var propname in obj) { +    if (typeof doc[propname] == "object") { +      recursivelySeal(doc[propname]); +    } +  } +} + +var responseSent; +// prints the object as JSON, and rescues and logs any toJSON() related errors +function respond(obj) { +  responseSent = true; +  try { +    print(toJSON(obj));   +  } catch(e) { +    log("Error converting object to JSON: " + e.toString()); +  } +}; + +log = function(message) { +  if (typeof message == "undefined") { +    message = "Error: attempting to log message of 'undefined'."; +  } else if (typeof message != "string") { +    message = toJSON(message); +  } +  print(toJSON({log: message})); +}; diff --git a/share/server/validate.js b/share/server/validate.js new file mode 100644 index 00000000..41d3c1e3 --- /dev/null +++ b/share/server/validate.js @@ -0,0 +1,23 @@ +// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the +// License for the specific language governing permissions and limitations under +// the License. + +var Validate = { +  validate : function(funSrc, newDoc, oldDoc, userCtx) { +    var validateFun = compileFunction(funSrc); +    try { +      validateFun(newDoc, oldDoc, userCtx); +      print("1"); +    } catch (error) { +      respond(error); +    } +  } +};
\ No newline at end of file diff --git a/share/server/views.js b/share/server/views.js new file mode 100644 index 00000000..c6d71579 --- /dev/null +++ b/share/server/views.js @@ -0,0 +1,117 @@ +// Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the +// License for the specific language governing permissions and limitations under +// the License. + +// globals used by views +var map_results = []; // holds temporary emitted values during doc map + +// view helper functions +emit = function(key, value) { +  map_results.push([key, value]); +} + +sum = function(values) { +  var rv = 0; +  for (var i in values) { +    rv += values[i]; +  } +  return rv; +} + +var Views = (function() { +     +  function runReduce(reduceFuns, keys, values, rereduce) { +    for (var i in reduceFuns) { +      reduceFuns[i] = compileFunction(reduceFuns[i]); +    } +    var reductions = new Array(reduceFuns.length); +    for(var i = 0; i < reduceFuns.length; i++) { +      try { +        reductions[i] = reduceFuns[i](keys, values, rereduce); +      } catch (err) { +        if (err == "fatal_error") { +          throw { +            error: "reduce_runtime_error", +            reason: "function raised fatal exception"}; +        } +        log("function raised exception (" + err + ")"); +        reductions[i] = null; +      } +    } +    print("[true," + toJSON(reductions) + "]"); +  }; +   +  return { +    reduce : function(reduceFuns, kvs) { +      var keys = new Array(kvs.length); +      var values = new Array(kvs.length); +      for(var i = 0; i < kvs.length; i++) { +          keys[i] = kvs[i][0]; +          values[i] = kvs[i][1]; +      } +      runReduce(reduceFuns, keys, values, false); +    }, +    rereduce : function(reduceFuns, values) { +      runReduce(reduceFuns, null, values, true); +    }, +    mapDoc : function(doc) { +      // Compute all the map functions against the document. +      // +      // Each function can output multiple key/value pairs for each document. +      // +      // Example output of map_doc after three functions set by add_fun cmds: +      // [ +      //  [["Key","Value"]],                    <- fun 1 returned 1 key value +      //  [],                                   <- fun 2 returned 0 key values +      //  [["Key1","Value1"],["Key2","Value2"]] <- fun 3 returned 2 key values +      // ] +      // + +      /* +      Immutable document support temporarily removed. + +      Removed because the seal function no longer works on JS 1.8 arrays, +      instead returning an error. The sealing is meant to prevent map +      functions from modifying the same document that is passed to other map +      functions. However, only map functions in the same design document are +      run together, so we have a reasonable expectation they can trust each +      other. Any map fun that can't be trusted can be placed in its own +      design document, and it cannot affect other map functions. + +      recursivelySeal(doc); // seal to prevent map functions from changing doc +      */ +      var buf = []; +      for (var i = 0; i < funs.length; i++) { +        map_results = []; +        try { +          funs[i](doc); +          buf.push(toJSON(map_results)); +        } catch (err) { +          if (err == "fatal_error") { +            // Only if it's a "fatal_error" do we exit. What's a fatal error? +            // That's for the query to decide. +            // +            // This will make it possible for queries to completely error out, +            // by catching their own local exception and rethrowing a +            // fatal_error. But by default if they don't do error handling we +            // just eat the exception and carry on. +            throw { +              error: "map_runtime_error", +              reason: "function raised fatal exception"}; +          } +          log("function raised exception (" + err + ") with doc._id " + doc._id); +          buf.push("[]"); +        } +      } +      print("[" + buf.join(", ") + "]"); +    } +  } +})(); diff --git a/share/www/script/test/list_views.js b/share/www/script/test/list_views.js index e71ebc4e..0bf03900 100644 --- a/share/www/script/test/list_views.js +++ b/share/www/script/test/list_views.js @@ -130,6 +130,15 @@ couchTests.list_views = function(debug) {        }),        emptyList: stringFun(function(head, row, req, row_info) {          return { body: "" }; +      }), +      rowError : stringFun(function(head, row, req, row_info) { +        if (head) { +          return "head"; +        } else if(row) { +          return missingValue; +        } else { +          return "tail" +        }        })      }    }; @@ -272,4 +281,7 @@ couchTests.list_views = function(debug) {    });    T(xhr.status == 400);    T(/query_parse_error/.test(xhr.responseText)); +   +  var xhr = CouchDB.request("GET", "/test_suite_db/_design/lists/_list/rowError/basicView"); +  T(/<h1>Render Error<\/h1>/.test(xhr.responseText));  }; diff --git a/share/www/script/test/show_documents.js b/share/www/script/test/show_documents.js index c755898e..bf12e4c6 100644 --- a/share/www/script/test/show_documents.js +++ b/share/www/script/test/show_documents.js @@ -54,6 +54,9 @@ couchTests.show_documents = function(debug) {            json : req          }        }), +      "render-error" : stringFun(function(doc, req) { +        return noSuchVariable; +      }),        "xml-type" : stringFun(function(doc, req) {           return {             "headers" : { @@ -142,6 +145,10 @@ couchTests.show_documents = function(debug) {    // hello template world    xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello/"+docid);    T(xhr.responseText == "Hello World"); + +  // error stacktraces +  xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/render-error/"+docid); +  T(JSON.parse(xhr.responseText).error == "render_error");    // hello template world (no docid)    xhr = CouchDB.request("GET", "/test_suite_db/_design/template/_show/hello"); diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl index a16f5fdd..5e150055 100644 --- a/src/couchdb/couch_httpd.erl +++ b/src/couchdb/couch_httpd.erl @@ -422,8 +422,8 @@ error_info({Error, Reason}) ->  error_info(Error) ->      {500, <<"unknown_error">>, couch_util:to_binary(Error)}. -send_error(_Req, {already_sent, _Error}) -> -    ok; +send_error(_Req, {already_sent, Resp, _Error}) -> +    {ok, Resp};  send_error(Req, Error) ->      {Code, ErrorStr, ReasonStr} = error_info(Error), @@ -447,21 +447,17 @@ send_error(Req, Code, Headers, ErrorStr, ReasonStr) ->          {[{<<"error">>,  ErrorStr},           {<<"reason">>, ReasonStr}]}). +% give the option for list functions to output html or other raw errors +send_chunked_error(Resp, {_Error, {[{<<"body">>, Reason}]}}) -> +    send_chunk(Resp, Reason), +    send_chunk(Resp, []); +  send_chunked_error(Resp, Error) ->      {Code, ErrorStr, ReasonStr} = error_info(Error), -    CType = Resp:get_header_value("Content-Type"), -    case CType of -        "text/html" -> -            HtmlError = ?l2b([$\n, -                "<html><body><h2>Error: ", ErrorStr, "</h2>", -                "<pre>Reason: ", ReasonStr, "</pre>", $\n]), -            send_chunk(Resp, HtmlError); -        _Else -> -            JsonError = {[{<<"code">>, Code}, -                {<<"error">>,  ErrorStr},  -                {<<"reason">>, ReasonStr}]}, -            send_chunk(Resp, ?l2b([$\n,?JSON_ENCODE(JsonError),$\n])) -    end, +    JsonError = {[{<<"code">>, Code}, +        {<<"error">>,  ErrorStr},  +        {<<"reason">>, ReasonStr}]}, +    send_chunk(Resp, ?l2b([$\n,?JSON_ENCODE(JsonError),$\n])),      send_chunk(Resp, []).  send_redirect(Req, Path) -> diff --git a/src/couchdb/couch_httpd_show.erl b/src/couchdb/couch_httpd_show.erl index 5aed8971..21611262 100644 --- a/src/couchdb/couch_httpd_show.erl +++ b/src/couchdb/couch_httpd_show.erl @@ -19,7 +19,7 @@  -import(couch_httpd,      [send_json/2,send_json/3,send_json/4,send_method_not_allowed/2, -    start_json_response/2,send_chunk/2, +    start_json_response/2,send_chunk/2,send_chunked_error/2,      start_chunked_response/3, send_error/4]).  handle_doc_show_req(#httpd{ @@ -132,24 +132,30 @@ make_map_start_resp_fun(QueryServer, Req, Db, CurrentEtag) ->  make_map_send_row_fun(QueryServer, Req) ->      fun(Resp, Db2, {{Key, DocId}, Value},           RowFront, _IncludeDocs) -> -        JsonResp = couch_query_servers:render_list_row(QueryServer,  -            Req, Db2, {{Key, DocId}, Value}), -        #extern_resp_args{ -            stop = StopIter, -            data = RowBody -        } = couch_httpd_external:parse_external_response(JsonResp), -        case StopIter of -        true -> stop; -        _ -> -            RowFront2 = case RowFront of -            nil -> []; -            _ -> RowFront -            end, -            Chunk = RowFront2 ++ binary_to_list(RowBody), -            case Chunk of -                [] -> {ok, Resp}; -                _ -> send_chunk(Resp, Chunk) +        try +            JsonResp = couch_query_servers:render_list_row(QueryServer,  +                Req, Db2, {{Key, DocId}, Value}), +            #extern_resp_args{ +                stop = StopIter, +                data = RowBody +            } = couch_httpd_external:parse_external_response(JsonResp), +            case StopIter of +            true -> stop; +            _ -> +                RowFront2 = case RowFront of +                nil -> []; +                _ -> RowFront +                end, +                Chunk = RowFront2 ++ binary_to_list(RowBody), +                case Chunk of +                    [] -> {ok, Resp}; +                    _ -> send_chunk(Resp, Chunk) +                end              end +        catch +            throw:Error -> +                send_chunked_error(Resp, Error), +                throw({already_sent, Resp, Error})          end      end. @@ -241,24 +247,30 @@ make_reduce_start_resp_fun(QueryServer, Req, Db, CurrentEtag) ->  make_reduce_send_row_fun(QueryServer, Req, Db) ->      fun(Resp, {Key, Value}, RowFront) -> -        JsonResp = couch_query_servers:render_reduce_row(QueryServer,  -            Req, Db, {Key, Value}), -        #extern_resp_args{ -            stop = StopIter, -            data = RowBody -        } = couch_httpd_external:parse_external_response(JsonResp), -        RowFront2 = case RowFront of -        nil -> []; -        _ -> RowFront -        end, -        case StopIter of -        true -> stop; -        _ -> -            Chunk = RowFront2 ++ binary_to_list(RowBody), -            case Chunk of -                [] -> {ok, Resp}; -                _ -> send_chunk(Resp, Chunk) +        try +            JsonResp = couch_query_servers:render_reduce_row(QueryServer,  +                Req, Db, {Key, Value}), +            #extern_resp_args{ +                stop = StopIter, +                data = RowBody +            } = couch_httpd_external:parse_external_response(JsonResp), +            RowFront2 = case RowFront of +            nil -> []; +            _ -> RowFront +            end, +            case StopIter of +            true -> stop; +            _ -> +                Chunk = RowFront2 ++ binary_to_list(RowBody), +                case Chunk of +                    [] -> {ok, Resp}; +                    _ -> send_chunk(Resp, Chunk) +                end              end +        catch +            throw:Error -> +                send_chunked_error(Resp, Error), +                throw({already_sent, Resp, Error})          end      end. @@ -417,3 +429,4 @@ apply_etag({ExternalResponse}, CurrentEtag) ->              Field          end || Field <- ExternalResponse]}      end. +    
\ No newline at end of file diff --git a/src/couchdb/couch_httpd_view.erl b/src/couchdb/couch_httpd_view.erl index 0348cf8b..8702a3dc 100644 --- a/src/couchdb/couch_httpd_view.erl +++ b/src/couchdb/couch_httpd_view.erl @@ -530,7 +530,7 @@ apply_default_helper_funs(#view_fold_helper_funs{      Helpers#view_fold_helper_funs{          passed_end = PassedEnd2,          start_response = StartResp2, -        send_row = wrap_for_chunked_errors(SendRow2) +        send_row = SendRow2      }.  apply_default_helper_funs(#reduce_fold_helper_funs{ @@ -549,7 +549,7 @@ apply_default_helper_funs(#reduce_fold_helper_funs{      Helpers#reduce_fold_helper_funs{          start_response = StartResp2, -        send_row = wrap_for_chunked_errors(SendRow2) +        send_row = SendRow2      }.  make_passed_end_fun(fwd, EndKey, EndDocId, InclusiveEnd) -> @@ -606,27 +606,7 @@ send_json_reduce_row(Resp, {Key, Value}, RowFront) ->      nil -> ",\r\n";      _ -> RowFront      end, -    send_chunk(Resp, RowFront2 ++ ?JSON_ENCODE({[{key, Key}, {value, Value}]})). - -wrap_for_chunked_errors(Fun) when is_function(Fun, 3)-> -    fun(Resp, B, C) -> -        try Fun(Resp, B, C) -        catch -            throw:Error -> -                send_chunked_error(Resp, Error), -                throw({already_sent, Error}) -        end -    end; - -wrap_for_chunked_errors(Fun) when is_function(Fun, 5)-> -    fun(Resp, B, C, D, E) -> -        try Fun(Resp, B, C, D, E) -        catch -            throw:Error -> -                send_chunked_error(Resp, Error), -                throw({already_sent, Error}) -        end -    end.     +    send_chunk(Resp, RowFront2 ++ ?JSON_ENCODE({[{key, Key}, {value, Value}]})).      view_group_etag(Group) ->      view_group_etag(Group, nil). | 
