diff --git a/src/index.ts b/src/index.ts
index e38e10a..3a12753 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -45,8 +45,9 @@ export {
} from './shared/http-utils.js';
export {
check,
- default as ServiceExceptionError,
-} from './shared/service-exception-error.js';
+ ServiceExceptionError,
+ EndpointError,
+} from './shared/errors.js';
export { enableFallbackWithoutWorker } from './worker/index.js';
import './worker-fallback/index.js';
diff --git a/src/ogc-api/endpoint.ts b/src/ogc-api/endpoint.ts
index 58a425e..a7efe4a 100644
--- a/src/ogc-api/endpoint.ts
+++ b/src/ogc-api/endpoint.ts
@@ -4,12 +4,12 @@ import {
checkStyleConformance,
checkTileConformance,
parseBaseCollectionInfo,
- parseFullStyleInfo,
+ parseBasicStyleInfo,
parseCollectionParameters,
parseCollections,
parseConformance,
parseEndpointInfo,
- parseBasicStyleInfo,
+ parseFullStyleInfo,
parseTileMatrixSets,
} from './info.js';
import {
diff --git a/src/shared/service-exception-error.spec.ts b/src/shared/errors.spec.ts
similarity index 98%
rename from src/shared/service-exception-error.spec.ts
rename to src/shared/errors.spec.ts
index e33a308..0f813c0 100644
--- a/src/shared/service-exception-error.spec.ts
+++ b/src/shared/errors.spec.ts
@@ -12,10 +12,7 @@ import wmsException110 from '../../fixtures/wms/service-exception-report-1-1-0.x
import wmsException111 from '../../fixtures/wms/service-exception-report-1-1-1.xml';
// @ts-expect-error ts-migrate(7016)
import wmsException130 from '../../fixtures/wms/service-exception-report-1-3-0.xml';
-import ServiceExceptionError, {
- check,
- parse,
-} from './service-exception-error.js';
+import { check, parse, ServiceExceptionError } from './errors.js';
import {
findChildElement,
getRootElement,
diff --git a/src/shared/errors.ts b/src/shared/errors.ts
index 7b866c0..367359d 100644
--- a/src/shared/errors.ts
+++ b/src/shared/errors.ts
@@ -1,9 +1,96 @@
+import type { XmlDocument, XmlElement } from '@rgrove/parse-xml';
+import {
+ findChildElement,
+ getElementAttribute,
+ getElementName,
+ getElementText,
+ getRootElement,
+ stripNamespace,
+} from '../shared/xml-utils.js';
+
export class EndpointError extends Error {
constructor(
- public message: string,
- public httpStatus?: number,
- public isCrossOriginRelated?: boolean
+ message: string,
+ public readonly httpStatus?: number,
+ public readonly isCrossOriginRelated?: boolean
) {
super(message);
}
}
+
+/**
+ * Representation of an Exception reported by an OWS service
+ *
+ * This is usually contained in a ServiceExceptionReport or ExceptionReport
+ * document and represented as a ServiceException or Exception element
+ */
+export class ServiceExceptionError extends Error {
+ /**
+ * Constructor
+ * @param message Error message
+ * @param requestUrl URL which resulted in the ServiceException
+ * @param code Optional ServiceException code
+ * @param locator Optional ServiceException locator
+ * @param response Optional response content received
+ */
+ public constructor(
+ message: string,
+ public readonly requestUrl?: string,
+ public readonly code?: string,
+ public readonly locator?: string,
+ public readonly response?: XmlDocument
+ ) {
+ super(message);
+ }
+}
+
+/**
+ * Parse a ServiceException element to a ServiceExceptionError
+ * @param serviceException ServiceException element
+ * @param url URL from which the ServiceException was generated
+ */
+export function parse(
+ serviceException: XmlElement,
+ url?: string
+): ServiceExceptionError {
+ const errorCode =
+ getElementAttribute(serviceException, 'code') ||
+ getElementAttribute(serviceException, 'exceptionCode');
+ const errorLocator = getElementAttribute(serviceException, 'locator');
+ const textElement =
+ findChildElement(serviceException, 'ExceptionText') || serviceException;
+ const errorMessage = getElementText(textElement).trim();
+ return new ServiceExceptionError(
+ errorMessage,
+ url,
+ errorCode,
+ errorLocator,
+ serviceException.document
+ );
+}
+
+/**
+ * Check the response for a ServiceExceptionReport and if present throw one
+ * @param response Response to check
+ * @param url URL from which response was generated
+ */
+export function check(response: XmlDocument, url?: string): XmlDocument {
+ const rootEl = getRootElement(response);
+ const rootElName = stripNamespace(getElementName(rootEl));
+ if (rootElName === 'ServiceExceptionReport') {
+ // document contains a ServiceExceptionReport, so generate an Error from
+ // the first ServiceException contained in it
+ const error = findChildElement(rootEl, 'ServiceException');
+ if (error) {
+ throw parse(error, url);
+ }
+ }
+ if (rootElName === 'ExceptionReport') {
+ const error = findChildElement(rootEl, 'Exception');
+ if (error) {
+ throw parse(error, url);
+ }
+ }
+ // there was nothing to convert to an Error so just pass the document on
+ return response;
+}
diff --git a/src/shared/http-utils.ts b/src/shared/http-utils.ts
index 641b6b0..0e9ce90 100644
--- a/src/shared/http-utils.ts
+++ b/src/shared/http-utils.ts
@@ -101,7 +101,7 @@ export function queryXmlDocument(url: string) {
);
})
)
- .then(async (resp) => {
+ .then(async (resp: Response) => {
if (!resp.ok) {
const text = await resp.text();
throw new EndpointError(
diff --git a/src/shared/service-exception-error.ts b/src/shared/service-exception-error.ts
deleted file mode 100644
index e02c9b8..0000000
--- a/src/shared/service-exception-error.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import type { XmlDocument, XmlElement } from '@rgrove/parse-xml';
-import {
- findChildElement,
- getElementAttribute,
- getElementName,
- getElementText,
- getRootElement,
- stripNamespace,
-} from '../shared/xml-utils.js';
-
-/**
- * Representation of an Exception reported by an OWS service
- *
- * This is usually contained in a ServiceExceptionReport or ExceptionReport
- * document and represented as a ServiceException or Exception element
- */
-export default class ServiceExceptionError extends Error {
- /**
- * Constructor
- * @param message Error message
- * @param requestUrl URL which resulted in the ServiceException
- * @param code Optional ServiceException code
- * @param locator Optional ServiceException locator
- * @param response Optional response content received
- */
- public constructor(
- message: string,
- public readonly requestUrl?: string,
- public readonly code?: string,
- public readonly locator?: string,
- public readonly response?: XmlDocument
- ) {
- super(message);
- }
-}
-
-/**
- * Parse a ServiceException element to a ServiceExceptionError
- * @param serviceException ServiceException element
- * @param url URL from which the ServiceException was generated
- */
-export function parse(
- serviceException: XmlElement,
- url?: string
-): ServiceExceptionError {
- const errorCode =
- getElementAttribute(serviceException, 'code') ||
- getElementAttribute(serviceException, 'exceptionCode');
- const errorLocator = getElementAttribute(serviceException, 'locator');
- const textElement =
- findChildElement(serviceException, 'ExceptionText') || serviceException;
- const errorMessage = getElementText(textElement).trim();
- return new ServiceExceptionError(
- errorMessage,
- url,
- errorCode,
- errorLocator,
- serviceException.document
- );
-}
-
-/**
- * Check the response for a ServiceExceptionReport and if present throw one
- * @param response Response to check
- * @param url URL from which response was generated
- */
-export function check(response: XmlDocument, url?: string): XmlDocument {
- const rootEl = getRootElement(response);
- const rootElName = stripNamespace(getElementName(rootEl));
- if (rootElName === 'ServiceExceptionReport') {
- // document contains a ServiceExceptionReport, so generate an Error from
- // the first ServiceException contained in it
- const error = findChildElement(rootEl, 'ServiceException');
- if (error) {
- throw parse(error, url);
- }
- }
- if (rootElName === 'ExceptionReport') {
- const error = findChildElement(rootEl, 'Exception');
- if (error) {
- throw parse(error, url);
- }
- }
- // there was nothing to convert to an Error so just pass the document on
- return response;
-}
diff --git a/src/wfs/endpoint.spec.ts b/src/wfs/endpoint.spec.ts
index 9806fb8..73e20d3 100644
--- a/src/wfs/endpoint.spec.ts
+++ b/src/wfs/endpoint.spec.ts
@@ -12,6 +12,7 @@ import capabilitiesStates from '../../fixtures/wfs/capabilities-states-2-0-0.xml
import exceptionReportWms from '../../fixtures/wfs/exception-report-wms.xml';
import WfsEndpoint from './endpoint.js';
import { useCache } from '../shared/cache.js';
+import { EndpointError, ServiceExceptionError } from '../shared/errors.js';
jest.mock('../shared/cache', () => ({
useCache: jest.fn((factory) => factory()),
@@ -28,6 +29,7 @@ describe('WfsEndpoint', () => {
});
beforeEach(() => {
+ global.fetchPreHandler = () => {};
global.fetchResponseFactory = (url) => {
if (url.indexOf('GetCapabilities') > -1) return capabilities200;
if (url.indexOf('GetFeature') > -1) {
@@ -74,15 +76,88 @@ describe('WfsEndpoint', () => {
await expect(endpoint.isReady()).resolves.toEqual(endpoint);
});
+ describe('CORS error handling', () => {
+ beforeEach(() => {
+ global.fetchPreHandler = (url, options) => {
+ if (options?.method === 'HEAD') return 'ok!';
+ throw new Error('CORS problem');
+ };
+ endpoint = new WfsEndpoint('https://my.test.service/ogc/wfs');
+ });
+ it('rejects with a relevant error', async () => {
+ const error = (await endpoint
+ .isReady()
+ .catch((e) => e)) as EndpointError;
+ expect(error.constructor.name).toBe('EndpointError');
+ expect(error.message).toBe(
+ 'The document could not be fetched due to CORS limitations'
+ );
+ expect(error.httpStatus).toBe(0);
+ expect(error.isCrossOriginRelated).toBe(true);
+ });
+ });
+
+ describe('endpoint error handling', () => {
+ beforeEach(() => {
+ global.fetchPreHandler = () => {
+ throw new TypeError('other kind of problem');
+ };
+ endpoint = new WfsEndpoint('https://my.test.service/ogc/wfs');
+ });
+ it('rejects with a relevant error', async () => {
+ const error = (await endpoint
+ .isReady()
+ .catch((e) => e)) as EndpointError;
+ expect(error.constructor.name).toBe('EndpointError');
+ expect(error.message).toBe(
+ 'Fetching the document failed either due to network errors or unreachable host, error is: other kind of problem'
+ );
+ expect(error.httpStatus).toBe(0);
+ expect(error.isCrossOriginRelated).toBe(false);
+ });
+ });
+
+ describe('http error handling', () => {
+ beforeEach(() => {
+ global.fetchPreHandler = () => ({
+ ok: false,
+ text: () => Promise.resolve('something broke in the server'),
+ status: 500,
+ statusText: 'Internal Server Error',
+ });
+ endpoint = new WfsEndpoint('https://my.test.service/ogc/wfs');
+ });
+ it('rejects with a relevant error', async () => {
+ const error = (await endpoint
+ .isReady()
+ .catch((e) => e)) as EndpointError;
+ expect(error.constructor.name).toBe('EndpointError');
+ expect(error.message).toBe(
+ 'Received an error with code 500: something broke in the server'
+ );
+ expect(error.httpStatus).toBe(500);
+ expect(error.isCrossOriginRelated).toBe(false);
+ });
+ });
+
describe('service exception handling', () => {
beforeEach(() => {
global.fetchResponseFactory = () => exceptionReportWms;
- endpoint = new WfsEndpoint('https://my.test.service/ogc/wms');
+ endpoint = new WfsEndpoint('https://my.test.service/ogc/wfs');
});
it('rejects when the endpoint returns an exception report', async () => {
- await expect(endpoint.isReady()).rejects.toThrow(
+ const error = (await endpoint
+ .isReady()
+ .catch((e) => e)) as ServiceExceptionError;
+ expect(error.constructor.name).toBe('ServiceExceptionError');
+ expect(error.message).toBe(
'msWFSDispatch(): WFS server error. WFS request not enabled. Check wfs/ows_enable_request settings.'
);
+ expect(error.requestUrl).toBe(
+ 'https://my.test.service/ogc/wfs?SERVICE=WFS&REQUEST=GetCapabilities'
+ );
+ expect(error.code).toBe('InvalidParameterValue');
+ expect(error.locator).toBe('request');
});
});
});
diff --git a/src/wms/endpoint.spec.ts b/src/wms/endpoint.spec.ts
index bf70ec4..2adcb93 100644
--- a/src/wms/endpoint.spec.ts
+++ b/src/wms/endpoint.spec.ts
@@ -6,6 +6,7 @@ import capabilitiesStates from '../../fixtures/wms/capabilities-states-1-3-0.xml
import exceptionReportWfs from '../../fixtures/wms/service-exception-report-wfs.xml';
import WmsEndpoint from './endpoint.js';
import { useCache } from '../shared/cache.js';
+import { EndpointError, ServiceExceptionError } from '../shared/errors.js';
jest.mock('../shared/cache', () => ({
useCache: jest.fn((factory) => factory()),
@@ -18,6 +19,7 @@ describe('WmsEndpoint', () => {
beforeEach(() => {
jest.clearAllMocks();
+ global.fetchPreHandler = () => {};
global.fetchResponseFactory = () => capabilities130;
endpoint = new WmsEndpoint(
'https://my.test.service/ogc/wms?service=wms&request=GetMap&aa=bb'
@@ -54,15 +56,89 @@ describe('WmsEndpoint', () => {
it('resolves with the endpoint object', async () => {
await expect(endpoint.isReady()).resolves.toEqual(endpoint);
});
+
+ describe('CORS error handling', () => {
+ beforeEach(() => {
+ global.fetchPreHandler = (url, options) => {
+ if (options?.method === 'HEAD') return 'ok!';
+ throw new Error('CORS problem');
+ };
+ endpoint = new WmsEndpoint('https://my.test.service/ogc/wms');
+ });
+ it('rejects with a relevant error', async () => {
+ const error = (await endpoint
+ .isReady()
+ .catch((e) => e)) as EndpointError;
+ expect(error.constructor.name).toBe('EndpointError');
+ expect(error.message).toBe(
+ 'The document could not be fetched due to CORS limitations'
+ );
+ expect(error.httpStatus).toBe(0);
+ expect(error.isCrossOriginRelated).toBe(true);
+ });
+ });
+
+ describe('endpoint error handling', () => {
+ beforeEach(() => {
+ global.fetchPreHandler = () => {
+ throw new TypeError('other kind of problem');
+ };
+ endpoint = new WmsEndpoint('https://my.test.service/ogc/wms');
+ });
+ it('rejects with a relevant error', async () => {
+ const error = (await endpoint
+ .isReady()
+ .catch((e) => e)) as EndpointError;
+ expect(error.constructor.name).toBe('EndpointError');
+ expect(error.message).toBe(
+ 'Fetching the document failed either due to network errors or unreachable host, error is: other kind of problem'
+ );
+ expect(error.httpStatus).toBe(0);
+ expect(error.isCrossOriginRelated).toBe(false);
+ });
+ });
+
+ describe('http error handling', () => {
+ beforeEach(() => {
+ global.fetchPreHandler = () => ({
+ ok: false,
+ text: () => Promise.resolve('something broke in the server'),
+ status: 500,
+ statusText: 'Internal Server Error',
+ });
+ endpoint = new WmsEndpoint('https://my.test.service/ogc/wms');
+ });
+ it('rejects with a relevant error', async () => {
+ const error = (await endpoint
+ .isReady()
+ .catch((e) => e)) as EndpointError;
+ expect(error.constructor.name).toBe('EndpointError');
+ expect(error.message).toBe(
+ 'Received an error with code 500: something broke in the server'
+ );
+ expect(error.httpStatus).toBe(500);
+ expect(error.isCrossOriginRelated).toBe(false);
+ });
+ });
+
describe('service exception handling', () => {
beforeEach(() => {
global.fetchResponseFactory = () => exceptionReportWfs;
- endpoint = new WmsEndpoint('https://my.test.service/ogc/wfs');
+ endpoint = new WmsEndpoint('https://my.test.service/ogc/wms');
});
it('rejects when the endpoint returns an exception report', async () => {
- await expect(endpoint.isReady()).rejects.toThrow(
+ const error = (await endpoint
+ .isReady()
+ .catch((e) => e)) as ServiceExceptionError;
+ expect(error.constructor.name).toBe('ServiceExceptionError');
+ expect(error.message).toBe(
'msWMSGetCapabilities(): WMS server error. WMS request not enabled. Check wms/ows_enable_request settings.'
);
+ expect(error.requestUrl).toBe(
+ 'https://my.test.service/ogc/wms?SERVICE=WMS&REQUEST=GetCapabilities'
+ );
+ expect(error.code).toBe('');
+ expect(error.locator).toBe('');
});
});
});
diff --git a/src/worker/worker.ts b/src/worker/worker.ts
index a34cd0e..ee3a392 100644
--- a/src/worker/worker.ts
+++ b/src/worker/worker.ts
@@ -1,6 +1,6 @@
import { addTaskHandler } from './utils.js';
import { queryXmlDocument, setFetchOptions } from '../shared/http-utils.js';
-import { check } from '../shared/service-exception-error.js';
+import { check } from '../shared/errors.js';
import * as wmsCapabilities from '../wms/capabilities.js';
import * as wfsCapabilities from '../wfs/capabilities.js';
import * as wmtsCapabilities from '../wmts/capabilities.js';
diff --git a/test-setup.ts b/test-setup.ts
index 0a388f6..4948ef6 100644
--- a/test-setup.ts
+++ b/test-setup.ts
@@ -2,24 +2,29 @@
import 'regenerator-runtime/runtime';
import mitt from 'mitt';
import * as util from 'util';
+import { TextDecoder } from 'util';
import CacheMock from 'browser-cache-mock';
import 'isomorphic-fetch';
-import { TextDecoder } from 'util';
import { Buffer } from './node_modules/buffer/index.js';
globalThis.Buffer = Buffer;
// mock the global fetch API
-window.fetchResponseFactory = (url) => '';
+window.fetchPreHandler = (url, options) => {};
+window.fetchResponseFactory = (url, options) => '';
window.originalFetch = window.fetch;
-window.mockFetch = jest.fn().mockImplementation((url) =>
- Promise.resolve({
- text: () => Promise.resolve(globalThis.fetchResponseFactory(url)),
+window.mockFetch = jest.fn().mockImplementation(async (url, options) => {
+ const preResult = await window.fetchPreHandler(url, options);
+ if (preResult) return preResult;
+ return {
+ text: () => Promise.resolve(globalThis.fetchResponseFactory(url, options)),
json: () =>
- Promise.resolve(JSON.parse(globalThis.fetchResponseFactory(url))),
+ Promise.resolve(
+ JSON.parse(globalThis.fetchResponseFactory(url, options))
+ ),
arrayBuffer: () =>
Promise.resolve(
- Buffer.from(globalThis.fetchResponseFactory(url), 'utf-8')
+ Buffer.from(globalThis.fetchResponseFactory(url, options), 'utf-8')
),
clone: function () {
return this;
@@ -27,8 +32,8 @@ window.mockFetch = jest.fn().mockImplementation((url) =>
status: 200,
ok: true,
headers: { get: () => null },
- })
-);
+ };
+});
window.fetch = window.mockFetch;
// reset fetch response to XML by default