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.

stephan 2020-05-28 13:27 trunk
Commit 6849d9a5579ac4b49ef431dba4a6431f3944a57c7d79f25827a6cda0386f33e3
2 files changed +1 -1 +36 -32
+1 -1
--- src/fileedit.c
+++ src/fileedit.c
@@ -1713,13 +1713,13 @@
17131713
(CIMINI_DRY_RUN & cimi.flags) ? "true" : "false");
17141714
if(blob_size(&manifest)>0){
17151715
CX(",\"manifest\": %!j", blob_str(&manifest));
17161716
}
17171717
CX("}");
1718
+end_cleanup:
17181719
db_end_transaction(0/*noting that dry-run mode will have already
17191720
** set this to rollback mode. */);
1720
-end_cleanup:
17211721
fossil_free(zNewUuid);
17221722
blob_reset(&err);
17231723
blob_reset(&manifest);
17241724
CheckinMiniInfo_cleanup(&cimi);
17251725
}
17261726
--- 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
--- src/fossil.fetch.js
+++ src/fossil.fetch.js
@@ -22,14 +22,15 @@
2222
- onload: callback(responseData) (default = output response to the
2323
console). In the context of the callback, the options object is
2424
"this", noting that this call may have amended the options object
2525
with state other than what the caller provided.
2626
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".
3132
3233
- method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!
3334
3435
- payload: anything acceptable by XHR2.send(ARG) (DOMString,
3536
Document, FormData, Blob, File, ArrayBuffer), or a plain object or
@@ -75,48 +76,39 @@
7576
"this". These can be used to, e.g., keep track of in-flight
7677
requests and update the UI accordingly, e.g. disabling/enabling DOM
7778
elements. Any exceptions triggered by beforesend/aftersend are
7879
caught and silently ignored.
7980
81
+ - timeout: integer in milliseconds specifying the XHR timeout
82
+ duration. Default = fossil.fetch.timeout.
83
+
8084
When an options object does not provide
8185
onload/onerror/beforesend/aftersend handlers of its own, this
8286
function falls to defaults which are member properties of this
8387
function with the same name, e.g. fossil.fetch.onload(). The
8488
default onload/onerror implementations route the data through the
8589
dev console and (for onerror()) through fossil.error(). The default
8690
beforesend/aftersend are no-ops. Individual pages may overwrite
8791
those members to provide default implementations suitable for the
8892
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.
8996
9097
Returns this object, noting that the XHR request is asynchronous,
9198
and still in transit (or has yet to be sent) when that happens.
9299
*/
93100
window.fossil.fetch = function f(uri,opt){
94101
const F = fossil;
95102
if(!f.onload){
96
- f.onload = (r)=>console.debug('ajax response:',r);
103
+ f.onload = (r)=>console.debug('fossil.fetch() XHR response:',r);
97104
}
98105
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.");
118110
};
119111
}/*f.onerror()*/
120112
if(!f.parseResponseHeaders){
121113
f.parseResponseHeaders = function(h){
122114
const rc = {};
@@ -155,50 +147,62 @@
155147
const url=[F.repoUrl(uri,opt.urlParams)],
156148
x=new XMLHttpRequest();
157149
if('POST'===opt.method && 'string'===typeof opt.contentType){
158150
x.setRequestHeader('Content-Type',opt.contentType);
159151
}
160
- x.open(opt.method||'GET', url.join(''), true);
161152
if('json'===opt.responseType){
162153
/* 'json' is an extension to the supported XHR.responseType
163154
list. We use it as a flag to tell us to JSON.parse()
164155
the response. */
165156
jsonResponse = true;
166157
x.responseType = 'text';
167158
}else{
168159
x.responseType = opt.responseType||'text';
169160
}
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;
171167
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+"."));
174175
return;
175176
}
176177
const orh = opt.responseHeaders;
177178
let head;
178179
if(true===orh){
179
- head = f.parseResponseHeaders(this.getAllResponseHeaders());
180
+ head = f.parseResponseHeaders(x.getAllResponseHeaders());
180181
}else if('string'===typeof orh){
181
- head = this.getResponseHeader(orh);
182
+ head = x.getResponseHeader(orh);
182183
}else if(orh instanceof Array){
183184
head = {};
184185
orh.forEach((s)=>{
185186
if('string' === typeof s) head[s.toLowerCase()] = x.getResponseHeader(s);
186187
});
187188
}
188189
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];
191192
if(head) args.push(head);
192193
opt.onload.apply(opt, args);
193194
}catch(e){
194195
opt.onerror(e);
195196
}
196197
};
197198
try{opt.beforesend()}catch(e){/*ignore*/}
199
+ x.open(opt.method||'GET', url.join(''), true);
200
+ x.timeout = +opt.timeout || f.timeout;
198201
if(undefined!==payload) x.send(payload);
199202
else x.send();
200203
return this;
201204
};
202205
203206
window.fossil.fetch.beforesend = function(){};
204207
window.fossil.fetch.aftersend = function(){};
208
+window.fossil.fetch.timeout = 15000/* Default timeout, in ms. */;
205209
--- 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

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button