summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Christopher Anderson <jchris@apache.org>2009-01-27 20:46:39 +0000
committerJohn Christopher Anderson <jchris@apache.org>2009-01-27 20:46:39 +0000
commitee4ba41be000abbf4d888d306c72b6e805fdc01b (patch)
treedc94fe1feff640ddbc39ead878ba75420a55917c
parent482d19963237d4640e08bbb25585f2016328a004 (diff)
Improved etag handling for show funcs and db_doc requests; main.js cleanup (baby steps); null doc allowed for show funcs
git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@738237 13f79535-47bb-0310-9956-ffa450edef68
-rw-r--r--share/server/main.js45
-rw-r--r--share/www/script/couch_tests.js15
-rw-r--r--src/couchdb/couch_doc.erl2
-rw-r--r--src/couchdb/couch_httpd.erl26
-rw-r--r--src/couchdb/couch_httpd_db.erl15
-rw-r--r--src/couchdb/couch_httpd_show.erl88
-rw-r--r--src/couchdb/couch_query_servers.erl5
7 files changed, 126 insertions, 70 deletions
diff --git a/share/server/main.js b/share/server/main.js
index 8e1f91cd..286fd9c0 100644
--- a/share/server/main.js
+++ b/share/server/main.js
@@ -29,7 +29,12 @@ sum = function(values) {
}
log = function(message) {
- print(toJSON({log: toJSON(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
@@ -135,7 +140,7 @@ var Mimeparse = (function() {
// this function provides a shortcut for managing responses by Accept header
respondWith = function(req, responders) {
- var accept = req.headers["Accept"];
+ var bestKey = null, accept = req.headers["Accept"];
if (accept) {
var provides = [];
for (key in responders) {
@@ -144,19 +149,17 @@ respondWith = function(req, responders) {
}
}
var bestMime = Mimeparse.bestMatch(provides, accept);
- var bestKey = keysByMime[bestMime];
- var rFunc = responders[bestKey];
- if (rFunc) {
- var resp = maybeWrapResponse(rFunc());
- resp["headers"] = resp["headers"] || {};
- resp["headers"]["Content-Type"] = bestMime;
- return resp;
- }
+ bestKey = keysByMime[bestMime];
+ }
+ 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});
}
- if (responders.fallback) {
- return responders[responders.fallback]();
- }
- throw({code:406, body:"Not Acceptable: "+accept});
}
// whoever registers last wins.
@@ -378,20 +381,30 @@ function maybeWrapResponse(resp) {
}
};
+var responseSent;
function runRenderFunction(renderFun, args) {
+ responseSent = false;
try {
var resp = renderFun.apply(null, args);
- respond(maybeWrapResponse(resp));
+ if (!responseSent) {
+ if (resp) {
+ respond(maybeWrapResponse(resp));
+ } else {
+ respond({error:"render_error",reason:"undefined response from render function"});
+ }
+ }
} catch(e) {
log("function raised error: "+e.toString());
log("stacktrace: "+e.stack);
+ respond({error:"render_error",reason:e});
}
};
// prints the object as JSON, and rescues and logs any toJSON() related errors
function respond(obj) {
+ responseSent = true;
try {
- print(toJSON(obj));
+ print(toJSON(obj));
} catch(e) {
log("Error converting object to JSON: " + e.toString());
}
diff --git a/share/www/script/couch_tests.js b/share/www/script/couch_tests.js
index 01f94927..8f76ff06 100644
--- a/share/www/script/couch_tests.js
+++ b/share/www/script/couch_tests.js
@@ -2307,10 +2307,12 @@ var tests = {
_id:"_design/template",
language: "javascript",
shows: {
- "hello" : stringFun(function() {
- return {
- body : "Hello World"
- };
+ "hello" : stringFun(function(doc) {
+ if (doc) {
+ return "Hello World";
+ } else {
+ return "Empty World";
+ }
}),
"just-name" : stringFun(function(doc, req) {
return {
@@ -2410,6 +2412,11 @@ var tests = {
// hello template world
xhr = CouchDB.request("GET", "/test_suite_db/_show/template/hello/"+docid);
T(xhr.responseText == "Hello World");
+
+ // hello template world (no docid)
+ xhr = CouchDB.request("GET", "/test_suite_db/_show/template/hello");
+ T(xhr.responseText == "Empty World");
+
// show with doc
xhr = CouchDB.request("GET", "/test_suite_db/_show/template/just-name/"+docid);
diff --git a/src/couchdb/couch_doc.erl b/src/couchdb/couch_doc.erl
index db1f2625..5bad7854 100644
--- a/src/couchdb/couch_doc.erl
+++ b/src/couchdb/couch_doc.erl
@@ -147,7 +147,7 @@ from_json_obj({Props}) ->
attachments = Bins
};
-from_json_obj(Other) ->
+from_json_obj(_Other) ->
throw({invalid_json_object, "Document must be a JSON object"}).
to_doc_info(FullDocInfo) ->
diff --git a/src/couchdb/couch_httpd.erl b/src/couchdb/couch_httpd.erl
index 996bcf49..acd6af40 100644
--- a/src/couchdb/couch_httpd.erl
+++ b/src/couchdb/couch_httpd.erl
@@ -17,7 +17,7 @@
-export([header_value/2,header_value/3,qs_value/2,qs_value/3,qs/1,path/1]).
-export([verify_is_server_admin/1,unquote/1,quote/1,recv/2]).
--export([parse_form/1,json_body/1,body/1,doc_etag/1]).
+-export([parse_form/1,json_body/1,body/1,doc_etag/1, make_etag/1, etag_respond/3]).
-export([primary_header_value/2,partition/1,serve_file/3]).
-export([start_chunked_response/3,send_chunk/2]).
-export([start_json_response/2, start_json_response/3, end_json_response/1]).
@@ -261,7 +261,29 @@ json_body(#httpd{mochi_req=MochiReq}) ->
?JSON_DECODE(MochiReq:recv_body(?MAX_DOC_SIZE)).
doc_etag(#doc{revs=[DiskRev|_]}) ->
- "\"" ++ binary_to_list(DiskRev) ++ "\"".
+ "\"" ++ binary_to_list(DiskRev) ++ "\"".
+
+make_etag(Term) ->
+ <<SigInt:128/integer>> = erlang:md5(term_to_binary(Term)),
+ list_to_binary("\"" ++ lists:flatten(io_lib:format("~.36B",[SigInt])) ++ "\"").
+
+etag_match(Req, CurrentEtag) when is_binary(CurrentEtag) ->
+ etag_match(Req, binary_to_list(CurrentEtag));
+
+etag_match(Req, CurrentEtag) ->
+ EtagsToMatch = string:tokens(
+ couch_httpd:header_value(Req, "If-None-Match", ""), ", "),
+ lists:member(CurrentEtag, EtagsToMatch).
+
+etag_respond(Req, CurrentEtag, RespFun) ->
+ case etag_match(Req, CurrentEtag) of
+ true ->
+ % the client has this in their cache.
+ couch_httpd:send_response(Req, 304, [{"Etag", CurrentEtag}], <<>>);
+ false ->
+ % Run the function.
+ RespFun()
+ end.
verify_is_server_admin(#httpd{user_ctx=#user_ctx{roles=Roles}}) ->
case lists:member(<<"_admin">>, Roles) of
diff --git a/src/couchdb/couch_httpd_db.erl b/src/couchdb/couch_httpd_db.erl
index 9ef7387c..73d90fe6 100644
--- a/src/couchdb/couch_httpd_db.erl
+++ b/src/couchdb/couch_httpd_db.erl
@@ -387,20 +387,13 @@ db_doc_req(#httpd{method='GET'}=Req, Db, DocId) ->
[] ->
Doc = couch_doc_open(Db, DocId, Rev, Options),
DiskEtag = couch_httpd:doc_etag(Doc),
- EtagsToMatch = string:tokens(
- couch_httpd:header_value(Req, "If-None-Match", ""), ", "),
- case lists:member(DiskEtag, EtagsToMatch) of
- true ->
- % the client has this in their cache.
- couch_httpd:send_response(Req, 304, [{"Etag", DiskEtag}], <<>>);
- false ->
- Headers =
- case Doc#doc.meta of
+ couch_httpd:etag_respond(Req, DiskEtag, fun() ->
+ Headers = case Doc#doc.meta of
[] -> [{"Etag", DiskEtag}]; % output etag only when we have no meta
_ -> []
end,
- send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options))
- end;
+ send_json(Req, 200, Headers, couch_doc:to_json_obj(Doc, Options))
+ end);
_ ->
{ok, Results} = couch_db:open_doc_revs(Db, DocId, Revs, Options),
{ok, Resp} = start_json_response(Req, 200),
diff --git a/src/couchdb/couch_httpd_show.erl b/src/couchdb/couch_httpd_show.erl
index 47d3fb46..f724098a 100644
--- a/src/couchdb/couch_httpd_show.erl
+++ b/src/couchdb/couch_httpd_show.erl
@@ -33,6 +33,16 @@ handle_doc_show_req(#httpd{
Doc = couch_httpd_db:couch_doc_open(Db, Docid, [], []),
send_doc_show_response(Lang, ShowSrc, Doc, Req, Db);
+handle_doc_show_req(#httpd{
+ method='GET',
+ path_parts=[_, _, DesignName, ShowName]
+ }=Req, Db) ->
+ DesignId = <<"_design/", DesignName/binary>>,
+ #doc{body={Props}} = couch_httpd_db:couch_doc_open(Db, DesignId, [], []),
+ Lang = proplists:get_value(<<"language">>, Props, <<"javascript">>),
+ ShowSrc = get_nested_json_value({Props}, [<<"shows">>, ShowName]),
+ send_doc_show_response(Lang, ShowSrc, nil, Req, Db);
+
handle_doc_show_req(#httpd{method='GET'}=Req, _Db) ->
send_error(Req, 404, <<"show_error">>, <<"Invalid path.">>);
@@ -158,45 +168,32 @@ finish_view_list(Req, Db, QueryServer, TotalRows,
throw(Error)
end.
+send_doc_show_response(Lang, ShowSrc, nil, #httpd{mochi_req=MReq}=Req, Db) ->
+ % compute etag with no doc
+ Headers = MReq:get(headers),
+ Hlist = mochiweb_headers:to_list(Headers),
+ Accept = proplists:get_value('Accept', Hlist),
+ CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, nil, Accept}),
+ couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
+ ExternalResp = couch_query_servers:render_doc_show(Lang, ShowSrc,
+ nil, Req, Db),
+ JsonResp = apply_etag(ExternalResp, CurrentEtag),
+ couch_httpd_external:send_external_response(Req, JsonResp)
+ end);
send_doc_show_response(Lang, ShowSrc, #doc{revs=[DocRev|_]}=Doc, #httpd{mochi_req=MReq}=Req, Db) ->
- % make a term with etag-effecting Req components, but not always changing ones.
+ % calculate the etag
Headers = MReq:get(headers),
Hlist = mochiweb_headers:to_list(Headers),
Accept = proplists:get_value('Accept', Hlist),
- <<SigInt:128/integer>> = erlang:md5(term_to_binary({Lang, ShowSrc, DocRev, Accept})),
- CurrentEtag = list_to_binary("\"" ++ lists:flatten(io_lib:format("form_~.36B",[SigInt])) ++ "\""),
- EtagsToMatch = string:tokens(
- couch_httpd:header_value(Req, "If-None-Match", ""), ", "),
- % We know our etag now
- case lists:member(binary_to_list(CurrentEtag), EtagsToMatch) of
- true ->
- % the client has this in their cache.
- couch_httpd:send_response(Req, 304, [{"Etag", CurrentEtag}], <<>>);
- false ->
- % Run the external form renderer.
- {JsonResponse} = couch_query_servers:render_doc_show(Lang, ShowSrc, Doc, Req, Db),
- % Here we embark on the delicate task of replacing or creating the
- % headers on the JsonResponse object. We need to control the Etag and
- % Vary headers. If the external function controls the Etag, we'd have to
- % run it to check for a match, which sort of defeats the purpose.
- JsonResponse2 = case proplists:get_value(<<"headers">>, JsonResponse, nil) of
- nil ->
- % no JSON headers
- % add our Etag and Vary headers to the response
- [{<<"headers">>, {[{<<"Etag">>, CurrentEtag}, {<<"Vary">>, <<"Accept">>}]}} | JsonResponse];
- {JsonHeaders} ->
- [case Field of
- {<<"headers">>, {JsonHeaders}} -> % add our headers
- JsonHeadersEtagged = set_or_replace_header({<<"Etag">>, CurrentEtag}, JsonHeaders),
- JsonHeadersVaried = set_or_replace_header({<<"Vary">>, <<"Accept">>}, JsonHeadersEtagged),
- {<<"headers">>, {JsonHeadersVaried}};
- _ -> % skip non-header fields
- Field
- end || Field <- JsonResponse]
- end,
- couch_httpd_external:send_external_response(Req, {JsonResponse2})
- end.
+ CurrentEtag = couch_httpd:make_etag({Lang, ShowSrc, DocRev, Accept}),
+ % We know our etag now
+ couch_httpd:etag_respond(Req, CurrentEtag, fun() ->
+ ExternalResp = couch_query_servers:render_doc_show(Lang, ShowSrc,
+ Doc, Req, Db),
+ JsonResp = apply_etag(ExternalResp, CurrentEtag),
+ couch_httpd_external:send_external_response(Req, JsonResp)
+ end).
set_or_replace_header(H, L) ->
set_or_replace_header(H, L, []).
@@ -209,4 +206,25 @@ set_or_replace_header({Key, NewValue}, [{OtherKey, OtherVal} | Headers], Acc) ->
set_or_replace_header({Key, NewValue}, Headers, [{OtherKey, OtherVal} | Acc]);
set_or_replace_header({Key, NewValue}, [], Acc) ->
% end of list, add ours
- [{Key, NewValue}|Acc]. \ No newline at end of file
+ [{Key, NewValue}|Acc].
+
+apply_etag({ExternalResponse}, CurrentEtag) ->
+ % Here we embark on the delicate task of replacing or creating the
+ % headers on the JsonResponse object. We need to control the Etag and
+ % Vary headers. If the external function controls the Etag, we'd have to
+ % run it to check for a match, which sort of defeats the purpose.
+ case proplists:get_value(<<"headers">>, ExternalResponse, nil) of
+ nil ->
+ % no JSON headers
+ % add our Etag and Vary headers to the response
+ {[{<<"headers">>, {[{<<"Etag">>, CurrentEtag}, {<<"Vary">>, <<"Accept">>}]}} | ExternalResponse]};
+ {JsonHeaders} ->
+ {[case Field of
+ {<<"headers">>, {JsonHeaders}} -> % add our headers
+ JsonHeadersEtagged = set_or_replace_header({<<"Etag">>, CurrentEtag}, JsonHeaders),
+ JsonHeadersVaried = set_or_replace_header({<<"Vary">>, <<"Accept">>}, JsonHeadersEtagged),
+ {<<"headers">>, {JsonHeadersVaried}};
+ _ -> % skip non-header fields
+ Field
+ end || Field <- ExternalResponse]}
+ end.
diff --git a/src/couchdb/couch_query_servers.erl b/src/couchdb/couch_query_servers.erl
index a6bb6e06..5eca63c2 100644
--- a/src/couchdb/couch_query_servers.erl
+++ b/src/couchdb/couch_query_servers.erl
@@ -125,7 +125,10 @@ validate_doc_update(Lang, FunSrc, EditDoc, DiskDoc, Ctx) ->
render_doc_show(Lang, ShowSrc, Doc, Req, Db) ->
Pid = get_os_process(Lang),
- JsonDoc = couch_doc:to_json_obj(Doc, [revs]),
+ JsonDoc = case Doc of
+ nil -> null;
+ _ -> couch_doc:to_json_obj(Doc, [revs])
+ end,
JsonReq = couch_httpd_external:json_req_obj(Req, Db),
try couch_os_process:prompt(Pid,
[<<"show_doc">>, ShowSrc, JsonDoc, JsonReq]) of