2021-04-09 15:15:28 +02:00
/ *
Copyright 2020 Bruno Windels < bruno @ windels.cloud >
Copyright 2020 The Matrix . org Foundation C . I . C .
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 .
* /
import { HomeServerError , ConnectionError } from "../error.js" ;
2021-11-21 16:58:16 +01:00
import type { RequestResult } from "../../platform/web/dom/request/fetch.js" ;
2021-12-01 11:39:42 +01:00
import type { ILogItem } from "../../logging/types" ;
2021-04-09 15:15:28 +02:00
2021-11-24 16:35:01 +01:00
export interface IHomeServerRequest {
abort ( ) : void ;
response ( ) : Promise < any > ;
}
export class HomeServerRequest implements IHomeServerRequest {
2021-12-01 11:39:42 +01:00
private readonly _log? : ILogItem ;
2021-11-21 16:58:16 +01:00
private _sourceRequest? : RequestResult ;
2021-11-23 13:45:22 +01:00
// as we add types for expected responses from hs, this could be a generic class instead
2021-11-23 13:39:33 +01:00
private readonly _promise : Promise < any > ;
2021-11-21 16:58:16 +01:00
2021-12-01 11:39:42 +01:00
constructor ( method : string , url : string , sourceRequest : RequestResult , log? : ILogItem ) {
2021-04-09 15:15:28 +02:00
this . _log = log ;
this . _sourceRequest = sourceRequest ;
this . _promise = sourceRequest . response ( ) . then ( response = > {
log ? . set ( "status" , response . status ) ;
// ok?
if ( response . status >= 200 && response . status < 300 ) {
log ? . finish ( ) ;
return response . body ;
} else {
if ( response . status >= 500 ) {
const err = new ConnectionError ( ` Internal Server Error ` ) ;
log ? . catch ( err ) ;
throw err ;
} else if ( response . status >= 400 && ! response . body ? . errcode ) {
const err = new ConnectionError ( ` HTTP error status ${ response . status } without errcode in body, assume this is a load balancer complaining the server is offline. ` ) ;
log ? . catch ( err ) ;
throw err ;
} else {
const err = new HomeServerError ( method , url , response . body , response . status ) ;
log ? . set ( "errcode" , err . errcode ) ;
log ? . catch ( err ) ;
throw err ;
}
}
} , err = > {
2021-04-09 15:24:10 +02:00
// if this._sourceRequest is still set,
// the abort error came not from calling abort here
2021-04-09 15:15:28 +02:00
if ( err . name === "AbortError" && this . _sourceRequest ) {
// The service worker sometimes (only on Firefox, on long, large request,
// perhaps it has its own timeout?) aborts the request, see #187.
// When it happens, the best thing to do seems to be to retry.
//
2021-04-09 15:24:10 +02:00
// In the service worker, we will also actively abort all
// ongoing requests when trying to get a new service worker to activate
2021-04-09 15:15:28 +02:00
// (this may surface in the app as a TypeError, which already gets mapped
// to a ConnectionError in the request function, or an AbortError,
2021-04-09 15:24:10 +02:00
// depending on the browser), as the service worker will only be
// replaced when there are no more (fetch) events for the
// current one to handle.
2021-04-09 15:15:28 +02:00
//
// In that case, the request function (in fetch.js) will check
2021-04-09 15:24:10 +02:00
// the haltRequests flag on the service worker handler, and
// block any new requests, as that would break the update process.
2021-04-09 15:15:28 +02:00
//
// So it is OK to return a ConnectionError here.
// If we're updating the service worker, the /versions polling will
2021-04-09 15:24:10 +02:00
// be blocked at the fetch level because haltRequests is set.
2021-04-09 15:15:28 +02:00
// And for #187, retrying is the right thing to do.
const err = new ConnectionError ( ` Service worker aborted, either updating or hit #187. ` ) ;
log ? . catch ( err ) ;
throw err ;
} else {
if ( err . name === "ConnectionError" ) {
log ? . set ( "timeout" , err . isTimeout ) ;
}
log ? . catch ( err ) ;
throw err ;
}
} ) ;
}
2021-11-21 16:58:16 +01:00
abort ( ) : void {
2021-04-09 15:15:28 +02:00
if ( this . _sourceRequest ) {
this . _log ? . set ( "aborted" , true ) ;
this . _sourceRequest . abort ( ) ;
// to mark that it was on purpose in above rejection handler
2021-11-21 16:58:16 +01:00
this . _sourceRequest = undefined ;
2021-04-09 15:15:28 +02:00
}
}
2021-11-23 13:39:33 +01:00
response ( ) : Promise < any > {
2021-04-09 15:15:28 +02:00
return this . _promise ;
}
}
import { Request as MockRequest } from "../../mocks/Request.js" ;
import { AbortError } from "../error.js" ;
export function tests() {
return {
"Response is passed through" : async assert = > {
const request = new MockRequest ( ) ;
const hsRequest = new HomeServerRequest ( "GET" , "https://hs.tld/foo" , request ) ;
request . respond ( 200 , "foo" ) ;
assert . equal ( await hsRequest . response ( ) , "foo" ) ;
} ,
"Unexpected AbortError is mapped to ConnectionError" : async assert = > {
const request = new MockRequest ( ) ;
const hsRequest = new HomeServerRequest ( "GET" , "https://hs.tld/foo" , request ) ;
request . reject ( new AbortError ( ) ) ;
await assert . rejects ( hsRequest . response ( ) , ConnectionError ) ;
} ,
"500 response is mapped to ConnectionError" : async assert = > {
const request = new MockRequest ( ) ;
const hsRequest = new HomeServerRequest ( "GET" , "https://hs.tld/foo" , request ) ;
request . respond ( 500 ) ;
await assert . rejects ( hsRequest . response ( ) , ConnectionError ) ;
} ,
"4xx response is mapped to HomeServerError" : async assert = > {
const request = new MockRequest ( ) ;
const hsRequest = new HomeServerRequest ( "GET" , "https://hs.tld/foo" , request ) ;
request . respond ( 400 , { errcode : "FOO" } ) ;
await assert . rejects ( hsRequest . response ( ) , HomeServerError ) ;
} ,
"4xx response without errcode is mapped to ConnectionError" : async assert = > {
const request = new MockRequest ( ) ;
const hsRequest = new HomeServerRequest ( "GET" , "https://hs.tld/foo" , request ) ;
request . respond ( 400 ) ;
await assert . rejects ( hsRequest . response ( ) , ConnectionError ) ;
} ,
"Other errors are passed through" : async assert = > {
class MyError extends Error { }
const request = new MockRequest ( ) ;
const hsRequest = new HomeServerRequest ( "GET" , "https://hs.tld/foo" , request ) ;
request . reject ( new MyError ( ) ) ;
await assert . rejects ( hsRequest . response ( ) , MyError ) ;
} ,
} ;
}