diff options
author | John Christopher Anderson <jchris@apache.org> | 2009-01-27 20:46:39 +0000 |
---|---|---|
committer | John Christopher Anderson <jchris@apache.org> | 2009-01-27 20:46:39 +0000 |
commit | ee4ba41be000abbf4d888d306c72b6e805fdc01b (patch) | |
tree | dc94fe1feff640ddbc39ead878ba75420a55917c | |
parent | 482d19963237d4640e08bbb25585f2016328a004 (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.js | 45 | ||||
-rw-r--r-- | share/www/script/couch_tests.js | 15 | ||||
-rw-r--r-- | src/couchdb/couch_doc.erl | 2 | ||||
-rw-r--r-- | src/couchdb/couch_httpd.erl | 26 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_db.erl | 15 | ||||
-rw-r--r-- | src/couchdb/couch_httpd_show.erl | 88 | ||||
-rw-r--r-- | src/couchdb/couch_query_servers.erl | 5 |
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 |