Fossil SCM
fossil.fetch() now uses onreadystatechange instead of onload because the latter does not handle connection errors and timeouts. Added timeout option to fossil.fetch() with a default of 15s. Corrected non-closing of a failed transaction when fileedit/commit fails.
Commit
6849d9a5579ac4b49ef431dba4a6431f3944a57c7d79f25827a6cda0386f33e3
Parent
13e26db40393a52…
2 files changed
+1
-1
+36
-32
+1
-1
| --- src/fileedit.c | ||
| +++ src/fileedit.c | ||
| @@ -1713,13 +1713,13 @@ | ||
| 1713 | 1713 | (CIMINI_DRY_RUN & cimi.flags) ? "true" : "false"); |
| 1714 | 1714 | if(blob_size(&manifest)>0){ |
| 1715 | 1715 | CX(",\"manifest\": %!j", blob_str(&manifest)); |
| 1716 | 1716 | } |
| 1717 | 1717 | CX("}"); |
| 1718 | +end_cleanup: | |
| 1718 | 1719 | db_end_transaction(0/*noting that dry-run mode will have already |
| 1719 | 1720 | ** set this to rollback mode. */); |
| 1720 | -end_cleanup: | |
| 1721 | 1721 | fossil_free(zNewUuid); |
| 1722 | 1722 | blob_reset(&err); |
| 1723 | 1723 | blob_reset(&manifest); |
| 1724 | 1724 | CheckinMiniInfo_cleanup(&cimi); |
| 1725 | 1725 | } |
| 1726 | 1726 |
| --- src/fileedit.c | |
| +++ src/fileedit.c | |
| @@ -1713,13 +1713,13 @@ | |
| 1713 | (CIMINI_DRY_RUN & cimi.flags) ? "true" : "false"); |
| 1714 | if(blob_size(&manifest)>0){ |
| 1715 | CX(",\"manifest\": %!j", blob_str(&manifest)); |
| 1716 | } |
| 1717 | CX("}"); |
| 1718 | db_end_transaction(0/*noting that dry-run mode will have already |
| 1719 | ** set this to rollback mode. */); |
| 1720 | end_cleanup: |
| 1721 | fossil_free(zNewUuid); |
| 1722 | blob_reset(&err); |
| 1723 | blob_reset(&manifest); |
| 1724 | CheckinMiniInfo_cleanup(&cimi); |
| 1725 | } |
| 1726 |
| --- src/fileedit.c | |
| +++ src/fileedit.c | |
| @@ -1713,13 +1713,13 @@ | |
| 1713 | (CIMINI_DRY_RUN & cimi.flags) ? "true" : "false"); |
| 1714 | if(blob_size(&manifest)>0){ |
| 1715 | CX(",\"manifest\": %!j", blob_str(&manifest)); |
| 1716 | } |
| 1717 | CX("}"); |
| 1718 | end_cleanup: |
| 1719 | db_end_transaction(0/*noting that dry-run mode will have already |
| 1720 | ** set this to rollback mode. */); |
| 1721 | fossil_free(zNewUuid); |
| 1722 | blob_reset(&err); |
| 1723 | blob_reset(&manifest); |
| 1724 | CheckinMiniInfo_cleanup(&cimi); |
| 1725 | } |
| 1726 |
+36
-32
| --- src/fossil.fetch.js | ||
| +++ src/fossil.fetch.js | ||
| @@ -22,14 +22,15 @@ | ||
| 22 | 22 | - onload: callback(responseData) (default = output response to the |
| 23 | 23 | console). In the context of the callback, the options object is |
| 24 | 24 | "this", noting that this call may have amended the options object |
| 25 | 25 | with state other than what the caller provided. |
| 26 | 26 | |
| 27 | - - onerror: callback(XHR onload event | exception) (default = event | |
| 28 | - or exception to the console). Triggered if the request generates | |
| 29 | - any response other than HTTP 200. In the context of the callback, | |
| 30 | - the options object is "this". | |
| 27 | + - onerror: callback(Error object) (default = output error message | |
| 28 | + to console.error() and fossil.error()). Triggered if the request | |
| 29 | + generates any response other than HTTP 200 or suffers a connection | |
| 30 | + error or timeout while awaiting a response. In the context of the | |
| 31 | + callback, the options object is "this". | |
| 31 | 32 | |
| 32 | 33 | - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE! |
| 33 | 34 | |
| 34 | 35 | - payload: anything acceptable by XHR2.send(ARG) (DOMString, |
| 35 | 36 | Document, FormData, Blob, File, ArrayBuffer), or a plain object or |
| @@ -75,48 +76,39 @@ | ||
| 75 | 76 | "this". These can be used to, e.g., keep track of in-flight |
| 76 | 77 | requests and update the UI accordingly, e.g. disabling/enabling DOM |
| 77 | 78 | elements. Any exceptions triggered by beforesend/aftersend are |
| 78 | 79 | caught and silently ignored. |
| 79 | 80 | |
| 81 | + - timeout: integer in milliseconds specifying the XHR timeout | |
| 82 | + duration. Default = fossil.fetch.timeout. | |
| 83 | + | |
| 80 | 84 | When an options object does not provide |
| 81 | 85 | onload/onerror/beforesend/aftersend handlers of its own, this |
| 82 | 86 | function falls to defaults which are member properties of this |
| 83 | 87 | function with the same name, e.g. fossil.fetch.onload(). The |
| 84 | 88 | default onload/onerror implementations route the data through the |
| 85 | 89 | dev console and (for onerror()) through fossil.error(). The default |
| 86 | 90 | beforesend/aftersend are no-ops. Individual pages may overwrite |
| 87 | 91 | those members to provide default implementations suitable for the |
| 88 | 92 | page's use, e.g. keeping track of how many in-flight |
| 93 | + | |
| 94 | + Note that this routine may add properties to the 2nd argument, so | |
| 95 | + that instance should not be kept around for later use. | |
| 89 | 96 | |
| 90 | 97 | Returns this object, noting that the XHR request is asynchronous, |
| 91 | 98 | and still in transit (or has yet to be sent) when that happens. |
| 92 | 99 | */ |
| 93 | 100 | window.fossil.fetch = function f(uri,opt){ |
| 94 | 101 | const F = fossil; |
| 95 | 102 | if(!f.onload){ |
| 96 | - f.onload = (r)=>console.debug('ajax response:',r); | |
| 103 | + f.onload = (r)=>console.debug('fossil.fetch() XHR response:',r); | |
| 97 | 104 | } |
| 98 | 105 | if(!f.onerror){ |
| 99 | - f.onerror = function(e/*event or exception*/){ | |
| 100 | - console.error("Ajax error:",e); | |
| 101 | - if(e instanceof Error){ | |
| 102 | - F.error('Exception:',e); | |
| 103 | - } | |
| 104 | - else if(e.originalTarget && e.originalTarget.responseType==='text'){ | |
| 105 | - const txt = e.originalTarget.responseText; | |
| 106 | - try{ | |
| 107 | - /* The convention from the /filepage_xyz routes is to | |
| 108 | - return error responses in JSON form if possible: | |
| 109 | - {error: "..."} | |
| 110 | - */ | |
| 111 | - const j = JSON.parse(txt); | |
| 112 | - console.error("Error JSON:",j); | |
| 113 | - if(j.error){ F.error(j.error) }; | |
| 114 | - }catch(e){/* Try harder */ | |
| 115 | - F.error(txt) | |
| 116 | - } | |
| 117 | - } | |
| 106 | + f.onerror = function(e/*exception*/){ | |
| 107 | + console.error("fossil.fetch() XHR error:",e); | |
| 108 | + if(e instanceof Error) F.error('Exception:',e); | |
| 109 | + else F.error("Unknown error in handling of XHR request."); | |
| 118 | 110 | }; |
| 119 | 111 | }/*f.onerror()*/ |
| 120 | 112 | if(!f.parseResponseHeaders){ |
| 121 | 113 | f.parseResponseHeaders = function(h){ |
| 122 | 114 | const rc = {}; |
| @@ -155,50 +147,62 @@ | ||
| 155 | 147 | const url=[F.repoUrl(uri,opt.urlParams)], |
| 156 | 148 | x=new XMLHttpRequest(); |
| 157 | 149 | if('POST'===opt.method && 'string'===typeof opt.contentType){ |
| 158 | 150 | x.setRequestHeader('Content-Type',opt.contentType); |
| 159 | 151 | } |
| 160 | - x.open(opt.method||'GET', url.join(''), true); | |
| 161 | 152 | if('json'===opt.responseType){ |
| 162 | 153 | /* 'json' is an extension to the supported XHR.responseType |
| 163 | 154 | list. We use it as a flag to tell us to JSON.parse() |
| 164 | 155 | the response. */ |
| 165 | 156 | jsonResponse = true; |
| 166 | 157 | x.responseType = 'text'; |
| 167 | 158 | }else{ |
| 168 | 159 | x.responseType = opt.responseType||'text'; |
| 169 | 160 | } |
| 170 | - x.onload = function(e){ | |
| 161 | + x.ontimeout = function(){ | |
| 162 | + try{opt.aftersend()}catch(e){/*ignore*/} | |
| 163 | + opt.onerror(new Error("XHR timeout of "+x.timeout+"ms expired.")); | |
| 164 | + }; | |
| 165 | + x.onreadystatechange = function(){ | |
| 166 | + if(XMLHttpRequest.DONE !== x.readyState) return; | |
| 171 | 167 | try{opt.aftersend()}catch(e){/*ignore*/} |
| 172 | - if(200!==this.status){ | |
| 173 | - opt.onerror(e); | |
| 168 | + if(200!==x.status){ | |
| 169 | + let err; | |
| 170 | + try{ | |
| 171 | + const j = JSON.parse(x.response); | |
| 172 | + if(j.error) err = new Error(j.error); | |
| 173 | + }catch(ex){/*ignore*/} | |
| 174 | + opt.onerror(err || new Error("HTTP response status "+x.status+".")); | |
| 174 | 175 | return; |
| 175 | 176 | } |
| 176 | 177 | const orh = opt.responseHeaders; |
| 177 | 178 | let head; |
| 178 | 179 | if(true===orh){ |
| 179 | - head = f.parseResponseHeaders(this.getAllResponseHeaders()); | |
| 180 | + head = f.parseResponseHeaders(x.getAllResponseHeaders()); | |
| 180 | 181 | }else if('string'===typeof orh){ |
| 181 | - head = this.getResponseHeader(orh); | |
| 182 | + head = x.getResponseHeader(orh); | |
| 182 | 183 | }else if(orh instanceof Array){ |
| 183 | 184 | head = {}; |
| 184 | 185 | orh.forEach((s)=>{ |
| 185 | 186 | if('string' === typeof s) head[s.toLowerCase()] = x.getResponseHeader(s); |
| 186 | 187 | }); |
| 187 | 188 | } |
| 188 | 189 | try{ |
| 189 | - const args = [(jsonResponse && this.response) | |
| 190 | - ? JSON.parse(this.response) : this.response]; | |
| 190 | + const args = [(jsonResponse && x.response) | |
| 191 | + ? JSON.parse(x.response) : x.response]; | |
| 191 | 192 | if(head) args.push(head); |
| 192 | 193 | opt.onload.apply(opt, args); |
| 193 | 194 | }catch(e){ |
| 194 | 195 | opt.onerror(e); |
| 195 | 196 | } |
| 196 | 197 | }; |
| 197 | 198 | try{opt.beforesend()}catch(e){/*ignore*/} |
| 199 | + x.open(opt.method||'GET', url.join(''), true); | |
| 200 | + x.timeout = +opt.timeout || f.timeout; | |
| 198 | 201 | if(undefined!==payload) x.send(payload); |
| 199 | 202 | else x.send(); |
| 200 | 203 | return this; |
| 201 | 204 | }; |
| 202 | 205 | |
| 203 | 206 | window.fossil.fetch.beforesend = function(){}; |
| 204 | 207 | window.fossil.fetch.aftersend = function(){}; |
| 208 | +window.fossil.fetch.timeout = 15000/* Default timeout, in ms. */; | |
| 205 | 209 |
| --- src/fossil.fetch.js | |
| +++ src/fossil.fetch.js | |
| @@ -22,14 +22,15 @@ | |
| 22 | - onload: callback(responseData) (default = output response to the |
| 23 | console). In the context of the callback, the options object is |
| 24 | "this", noting that this call may have amended the options object |
| 25 | with state other than what the caller provided. |
| 26 | |
| 27 | - onerror: callback(XHR onload event | exception) (default = event |
| 28 | or exception to the console). Triggered if the request generates |
| 29 | any response other than HTTP 200. In the context of the callback, |
| 30 | the options object is "this". |
| 31 | |
| 32 | - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE! |
| 33 | |
| 34 | - payload: anything acceptable by XHR2.send(ARG) (DOMString, |
| 35 | Document, FormData, Blob, File, ArrayBuffer), or a plain object or |
| @@ -75,48 +76,39 @@ | |
| 75 | "this". These can be used to, e.g., keep track of in-flight |
| 76 | requests and update the UI accordingly, e.g. disabling/enabling DOM |
| 77 | elements. Any exceptions triggered by beforesend/aftersend are |
| 78 | caught and silently ignored. |
| 79 | |
| 80 | When an options object does not provide |
| 81 | onload/onerror/beforesend/aftersend handlers of its own, this |
| 82 | function falls to defaults which are member properties of this |
| 83 | function with the same name, e.g. fossil.fetch.onload(). The |
| 84 | default onload/onerror implementations route the data through the |
| 85 | dev console and (for onerror()) through fossil.error(). The default |
| 86 | beforesend/aftersend are no-ops. Individual pages may overwrite |
| 87 | those members to provide default implementations suitable for the |
| 88 | page's use, e.g. keeping track of how many in-flight |
| 89 | |
| 90 | Returns this object, noting that the XHR request is asynchronous, |
| 91 | and still in transit (or has yet to be sent) when that happens. |
| 92 | */ |
| 93 | window.fossil.fetch = function f(uri,opt){ |
| 94 | const F = fossil; |
| 95 | if(!f.onload){ |
| 96 | f.onload = (r)=>console.debug('ajax response:',r); |
| 97 | } |
| 98 | if(!f.onerror){ |
| 99 | f.onerror = function(e/*event or exception*/){ |
| 100 | console.error("Ajax error:",e); |
| 101 | if(e instanceof Error){ |
| 102 | F.error('Exception:',e); |
| 103 | } |
| 104 | else if(e.originalTarget && e.originalTarget.responseType==='text'){ |
| 105 | const txt = e.originalTarget.responseText; |
| 106 | try{ |
| 107 | /* The convention from the /filepage_xyz routes is to |
| 108 | return error responses in JSON form if possible: |
| 109 | {error: "..."} |
| 110 | */ |
| 111 | const j = JSON.parse(txt); |
| 112 | console.error("Error JSON:",j); |
| 113 | if(j.error){ F.error(j.error) }; |
| 114 | }catch(e){/* Try harder */ |
| 115 | F.error(txt) |
| 116 | } |
| 117 | } |
| 118 | }; |
| 119 | }/*f.onerror()*/ |
| 120 | if(!f.parseResponseHeaders){ |
| 121 | f.parseResponseHeaders = function(h){ |
| 122 | const rc = {}; |
| @@ -155,50 +147,62 @@ | |
| 155 | const url=[F.repoUrl(uri,opt.urlParams)], |
| 156 | x=new XMLHttpRequest(); |
| 157 | if('POST'===opt.method && 'string'===typeof opt.contentType){ |
| 158 | x.setRequestHeader('Content-Type',opt.contentType); |
| 159 | } |
| 160 | x.open(opt.method||'GET', url.join(''), true); |
| 161 | if('json'===opt.responseType){ |
| 162 | /* 'json' is an extension to the supported XHR.responseType |
| 163 | list. We use it as a flag to tell us to JSON.parse() |
| 164 | the response. */ |
| 165 | jsonResponse = true; |
| 166 | x.responseType = 'text'; |
| 167 | }else{ |
| 168 | x.responseType = opt.responseType||'text'; |
| 169 | } |
| 170 | x.onload = function(e){ |
| 171 | try{opt.aftersend()}catch(e){/*ignore*/} |
| 172 | if(200!==this.status){ |
| 173 | opt.onerror(e); |
| 174 | return; |
| 175 | } |
| 176 | const orh = opt.responseHeaders; |
| 177 | let head; |
| 178 | if(true===orh){ |
| 179 | head = f.parseResponseHeaders(this.getAllResponseHeaders()); |
| 180 | }else if('string'===typeof orh){ |
| 181 | head = this.getResponseHeader(orh); |
| 182 | }else if(orh instanceof Array){ |
| 183 | head = {}; |
| 184 | orh.forEach((s)=>{ |
| 185 | if('string' === typeof s) head[s.toLowerCase()] = x.getResponseHeader(s); |
| 186 | }); |
| 187 | } |
| 188 | try{ |
| 189 | const args = [(jsonResponse && this.response) |
| 190 | ? JSON.parse(this.response) : this.response]; |
| 191 | if(head) args.push(head); |
| 192 | opt.onload.apply(opt, args); |
| 193 | }catch(e){ |
| 194 | opt.onerror(e); |
| 195 | } |
| 196 | }; |
| 197 | try{opt.beforesend()}catch(e){/*ignore*/} |
| 198 | if(undefined!==payload) x.send(payload); |
| 199 | else x.send(); |
| 200 | return this; |
| 201 | }; |
| 202 | |
| 203 | window.fossil.fetch.beforesend = function(){}; |
| 204 | window.fossil.fetch.aftersend = function(){}; |
| 205 |
| --- src/fossil.fetch.js | |
| +++ src/fossil.fetch.js | |
| @@ -22,14 +22,15 @@ | |
| 22 | - onload: callback(responseData) (default = output response to the |
| 23 | console). In the context of the callback, the options object is |
| 24 | "this", noting that this call may have amended the options object |
| 25 | with state other than what the caller provided. |
| 26 | |
| 27 | - onerror: callback(Error object) (default = output error message |
| 28 | to console.error() and fossil.error()). Triggered if the request |
| 29 | generates any response other than HTTP 200 or suffers a connection |
| 30 | error or timeout while awaiting a response. In the context of the |
| 31 | callback, the options object is "this". |
| 32 | |
| 33 | - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE! |
| 34 | |
| 35 | - payload: anything acceptable by XHR2.send(ARG) (DOMString, |
| 36 | Document, FormData, Blob, File, ArrayBuffer), or a plain object or |
| @@ -75,48 +76,39 @@ | |
| 76 | "this". These can be used to, e.g., keep track of in-flight |
| 77 | requests and update the UI accordingly, e.g. disabling/enabling DOM |
| 78 | elements. Any exceptions triggered by beforesend/aftersend are |
| 79 | caught and silently ignored. |
| 80 | |
| 81 | - timeout: integer in milliseconds specifying the XHR timeout |
| 82 | duration. Default = fossil.fetch.timeout. |
| 83 | |
| 84 | When an options object does not provide |
| 85 | onload/onerror/beforesend/aftersend handlers of its own, this |
| 86 | function falls to defaults which are member properties of this |
| 87 | function with the same name, e.g. fossil.fetch.onload(). The |
| 88 | default onload/onerror implementations route the data through the |
| 89 | dev console and (for onerror()) through fossil.error(). The default |
| 90 | beforesend/aftersend are no-ops. Individual pages may overwrite |
| 91 | those members to provide default implementations suitable for the |
| 92 | page's use, e.g. keeping track of how many in-flight |
| 93 | |
| 94 | Note that this routine may add properties to the 2nd argument, so |
| 95 | that instance should not be kept around for later use. |
| 96 | |
| 97 | Returns this object, noting that the XHR request is asynchronous, |
| 98 | and still in transit (or has yet to be sent) when that happens. |
| 99 | */ |
| 100 | window.fossil.fetch = function f(uri,opt){ |
| 101 | const F = fossil; |
| 102 | if(!f.onload){ |
| 103 | f.onload = (r)=>console.debug('fossil.fetch() XHR response:',r); |
| 104 | } |
| 105 | if(!f.onerror){ |
| 106 | f.onerror = function(e/*exception*/){ |
| 107 | console.error("fossil.fetch() XHR error:",e); |
| 108 | if(e instanceof Error) F.error('Exception:',e); |
| 109 | else F.error("Unknown error in handling of XHR request."); |
| 110 | }; |
| 111 | }/*f.onerror()*/ |
| 112 | if(!f.parseResponseHeaders){ |
| 113 | f.parseResponseHeaders = function(h){ |
| 114 | const rc = {}; |
| @@ -155,50 +147,62 @@ | |
| 147 | const url=[F.repoUrl(uri,opt.urlParams)], |
| 148 | x=new XMLHttpRequest(); |
| 149 | if('POST'===opt.method && 'string'===typeof opt.contentType){ |
| 150 | x.setRequestHeader('Content-Type',opt.contentType); |
| 151 | } |
| 152 | if('json'===opt.responseType){ |
| 153 | /* 'json' is an extension to the supported XHR.responseType |
| 154 | list. We use it as a flag to tell us to JSON.parse() |
| 155 | the response. */ |
| 156 | jsonResponse = true; |
| 157 | x.responseType = 'text'; |
| 158 | }else{ |
| 159 | x.responseType = opt.responseType||'text'; |
| 160 | } |
| 161 | x.ontimeout = function(){ |
| 162 | try{opt.aftersend()}catch(e){/*ignore*/} |
| 163 | opt.onerror(new Error("XHR timeout of "+x.timeout+"ms expired.")); |
| 164 | }; |
| 165 | x.onreadystatechange = function(){ |
| 166 | if(XMLHttpRequest.DONE !== x.readyState) return; |
| 167 | try{opt.aftersend()}catch(e){/*ignore*/} |
| 168 | if(200!==x.status){ |
| 169 | let err; |
| 170 | try{ |
| 171 | const j = JSON.parse(x.response); |
| 172 | if(j.error) err = new Error(j.error); |
| 173 | }catch(ex){/*ignore*/} |
| 174 | opt.onerror(err || new Error("HTTP response status "+x.status+".")); |
| 175 | return; |
| 176 | } |
| 177 | const orh = opt.responseHeaders; |
| 178 | let head; |
| 179 | if(true===orh){ |
| 180 | head = f.parseResponseHeaders(x.getAllResponseHeaders()); |
| 181 | }else if('string'===typeof orh){ |
| 182 | head = x.getResponseHeader(orh); |
| 183 | }else if(orh instanceof Array){ |
| 184 | head = {}; |
| 185 | orh.forEach((s)=>{ |
| 186 | if('string' === typeof s) head[s.toLowerCase()] = x.getResponseHeader(s); |
| 187 | }); |
| 188 | } |
| 189 | try{ |
| 190 | const args = [(jsonResponse && x.response) |
| 191 | ? JSON.parse(x.response) : x.response]; |
| 192 | if(head) args.push(head); |
| 193 | opt.onload.apply(opt, args); |
| 194 | }catch(e){ |
| 195 | opt.onerror(e); |
| 196 | } |
| 197 | }; |
| 198 | try{opt.beforesend()}catch(e){/*ignore*/} |
| 199 | x.open(opt.method||'GET', url.join(''), true); |
| 200 | x.timeout = +opt.timeout || f.timeout; |
| 201 | if(undefined!==payload) x.send(payload); |
| 202 | else x.send(); |
| 203 | return this; |
| 204 | }; |
| 205 | |
| 206 | window.fossil.fetch.beforesend = function(){}; |
| 207 | window.fossil.fetch.aftersend = function(){}; |
| 208 | window.fossil.fetch.timeout = 15000/* Default timeout, in ms. */; |
| 209 |