├── app
├── .gitkeep
└── services
│ └── fetch.js
├── addon
├── .gitkeep
├── fetch-request.js
├── services
│ └── fetch.js
├── -private
│ ├── utils
│ │ ├── is-string.js
│ │ ├── json-helpers.js
│ │ └── url-helpers.js
│ └── constants
│ │ └── response.js
├── request.js
├── errors.js
└── mixins
│ └── fetch-request.js
├── tests
├── unit
│ ├── .gitkeep
│ ├── services
│ │ └── fetch-test.js
│ ├── request-test.js
│ ├── errors-test.js
│ └── mixins
│ │ └── fetch-request-test.js
├── integration
│ └── .gitkeep
├── dummy
│ ├── app
│ │ ├── helpers
│ │ │ └── .gitkeep
│ │ ├── models
│ │ │ └── .gitkeep
│ │ ├── routes
│ │ │ └── .gitkeep
│ │ ├── components
│ │ │ └── .gitkeep
│ │ ├── controllers
│ │ │ └── .gitkeep
│ │ ├── styles
│ │ │ └── app.css
│ │ ├── templates
│ │ │ ├── application.hbs
│ │ │ ├── not-found.hbs
│ │ │ ├── docs.hbs
│ │ │ ├── docs
│ │ │ │ ├── index.md
│ │ │ │ ├── abort.md
│ │ │ │ └── usage.md
│ │ │ └── index.hbs
│ │ ├── app.js
│ │ ├── router.js
│ │ └── index.html
│ ├── public
│ │ └── robots.txt
│ └── config
│ │ ├── optional-features.json
│ │ ├── targets.js
│ │ ├── addon-docs.js
│ │ ├── ember-cli-update.json
│ │ ├── deploy.js
│ │ ├── environment.js
│ │ └── ember-try.js
├── helpers
│ ├── json.js
│ └── index.js
├── test-helper.js
└── index.html
├── .watchmanconfig
├── .template-lintrc.js
├── .stylelintrc.js
├── .stylelintignore
├── .prettierignore
├── .prettierrc.js
├── .eslintignore
├── .ember-cli
├── .gitignore
├── .editorconfig
├── index.js
├── testem.js
├── .npmignore
├── CONTRIBUTING.md
├── README.md
├── ember-cli-build.js
├── LICENSE.md
├── .eslintrc.js
├── .github
└── workflows
│ └── ci.yml
├── RELEASE.md
├── CHANGELOG.md
└── package.json
/app/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/addon/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/unit/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/integration/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/helpers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/routes/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/components/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/dummy/app/controllers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {
2 | "ignore_dirs": ["dist"]
3 | }
4 |
--------------------------------------------------------------------------------
/tests/dummy/app/styles/app.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --brand-primary: #00a85d;
3 | }
4 |
--------------------------------------------------------------------------------
/app/services/fetch.js:
--------------------------------------------------------------------------------
1 | export { default } from 'ember-ajax-fetch/services/fetch';
2 |
--------------------------------------------------------------------------------
/tests/dummy/public/robots.txt:
--------------------------------------------------------------------------------
1 | # http://www.robotstxt.org
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/.template-lintrc.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | extends: 'recommended',
5 | };
6 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/application.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{outlet}}
4 |
5 |
--------------------------------------------------------------------------------
/.stylelintrc.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | extends: ['stylelint-config-standard', 'stylelint-prettier/recommended'],
5 | };
6 |
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | # unconventional files
2 | /blueprints/*/files/
3 |
4 | # compiled output
5 | /dist/
6 |
7 | # addons
8 | /.node_modules.ember-try/
9 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/not-found.hbs:
--------------------------------------------------------------------------------
1 |
2 |
Not found
3 |
This page doesn't exist. Head home?
4 |
--------------------------------------------------------------------------------
/addon/fetch-request.js:
--------------------------------------------------------------------------------
1 | import EmberObject from '@ember/object';
2 | import FetchRequestMixin from './mixins/fetch-request';
3 |
4 | export default EmberObject.extend(FetchRequestMixin);
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # unconventional js
2 | /blueprints/*/files/
3 |
4 | # compiled output
5 | /dist/
6 |
7 | # misc
8 | /coverage/
9 | !.*
10 | .*/
11 |
12 | # ember-try
13 | /.node_modules.ember-try/
14 |
--------------------------------------------------------------------------------
/tests/dummy/config/optional-features.json:
--------------------------------------------------------------------------------
1 | {
2 | "application-template-wrapper": false,
3 | "default-async-observers": true,
4 | "jquery-integration": false,
5 | "template-only-glimmer-components": true
6 | }
7 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | overrides: [
5 | {
6 | files: '*.{js,ts}',
7 | options: {
8 | singleQuote: true,
9 | },
10 | },
11 | ],
12 | };
13 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | # unconventional js
2 | /blueprints/*/files/
3 |
4 | # compiled output
5 | /declarations/
6 | /dist/
7 |
8 | # misc
9 | /coverage/
10 | !.*
11 | .*/
12 |
13 | # ember-try
14 | /.node_modules.ember-try/
15 |
--------------------------------------------------------------------------------
/tests/dummy/config/targets.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const browsers = [
4 | 'last 1 Chrome versions',
5 | 'last 1 Firefox versions',
6 | 'last 1 Safari versions',
7 | ];
8 |
9 | module.exports = {
10 | browsers,
11 | };
12 |
--------------------------------------------------------------------------------
/addon/services/fetch.js:
--------------------------------------------------------------------------------
1 | import Service from '@ember/service';
2 | import FetchRequestMixin from '../mixins/fetch-request';
3 |
4 | /**
5 | * @class FetchService
6 | */
7 | const FetchService = Service.extend(FetchRequestMixin);
8 |
9 | export default FetchService;
10 |
--------------------------------------------------------------------------------
/.ember-cli:
--------------------------------------------------------------------------------
1 | {
2 | /**
3 | Setting `isTypeScriptProject` to true will force the blueprint generators to generate TypeScript
4 | rather than JavaScript by default, when a TypeScript version of a given blueprint is available.
5 | */
6 | "isTypeScriptProject": false
7 | }
8 |
--------------------------------------------------------------------------------
/addon/-private/utils/is-string.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Checks if the passed value is a string
3 | * @param {*} object
4 | * @return {boolean}
5 | * @function isString
6 | * @private
7 | */
8 | export default function isString(object) {
9 | return typeof object === 'string';
10 | }
11 |
--------------------------------------------------------------------------------
/addon/-private/constants/response.js:
--------------------------------------------------------------------------------
1 | export const COMPRESSED_TYPES = [
2 | 'application/zip',
3 | 'application/x-tar',
4 | 'application/gzip',
5 | 'application/x-bzip2',
6 | 'application/x-7z-compressed',
7 | 'application/x-rar-compressed',
8 | 'application/x-xz',
9 | 'application/vnd.ms-cab-compressed',
10 | ];
11 |
--------------------------------------------------------------------------------
/tests/dummy/config/addon-docs.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | 'use strict';
3 |
4 | const AddonDocsConfig = require('ember-cli-addon-docs/lib/config');
5 |
6 | module.exports = class extends AddonDocsConfig {
7 | // See https://ember-learn.github.io/ember-cli-addon-docs/docs/deploying
8 | // for details on configuration you can override here.
9 | };
10 |
--------------------------------------------------------------------------------
/tests/helpers/json.js:
--------------------------------------------------------------------------------
1 | export function jsonResponse(status = 200, payload = {}) {
2 | return [
3 | status,
4 | { 'Content-Type': 'application/json' },
5 | JSON.stringify(payload),
6 | ];
7 | }
8 |
9 | export function jsonFactory(status, payload) {
10 | return function json() {
11 | return jsonResponse(status, payload);
12 | };
13 | }
14 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{nav.section "Introduction"}}
4 | {{nav.item "Introduction" "docs.index"}}
5 | {{nav.item "Usage" "docs.usage"}}
6 | {{nav.item "Aborting Requests" "docs.abort"}}
7 |
8 |
9 |
10 | {{outlet}}
11 |
12 |
--------------------------------------------------------------------------------
/tests/unit/services/fetch-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { setupTest } from 'ember-qunit';
3 |
4 | module('Unit | Service | fetch', function (hooks) {
5 | setupTest(hooks);
6 |
7 | // Replace this with your real tests.
8 | test('it exists', function (assert) {
9 | let service = this.owner.lookup('service:fetch');
10 | assert.ok(service);
11 | });
12 | });
13 |
--------------------------------------------------------------------------------
/addon/request.js:
--------------------------------------------------------------------------------
1 | import FetchRequest from './fetch-request';
2 |
3 | /**
4 | * Helper function that allows you to use the default `ember-ajax-fetch` to make
5 | * requests without using the service.
6 | *
7 | * @class request
8 | * @public
9 | */
10 | export default function request(url, options) {
11 | const fetch = FetchRequest.create();
12 |
13 | return fetch.request(url, options);
14 | }
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist/
3 | /declarations/
4 |
5 | # dependencies
6 | /node_modules/
7 |
8 | # misc
9 | /.env*
10 | /.pnp*
11 | /.eslintcache
12 | /coverage/
13 | /npm-debug.log*
14 | /testem.log
15 | /yarn-error.log
16 |
17 | # ember-try
18 | /.node_modules.ember-try/
19 | /npm-shrinkwrap.json.ember-try
20 | /package.json.ember-try
21 | /package-lock.json.ember-try
22 | /yarn.lock.ember-try
23 |
24 | # broccoli-debug
25 | /DEBUG/
26 |
--------------------------------------------------------------------------------
/tests/dummy/app/app.js:
--------------------------------------------------------------------------------
1 | import Application from '@ember/application';
2 | import Resolver from 'ember-resolver';
3 | import loadInitializers from 'ember-load-initializers';
4 | import config from 'dummy/config/environment';
5 |
6 | export default class App extends Application {
7 | modulePrefix = config.modulePrefix;
8 | podModulePrefix = config.podModulePrefix;
9 | Resolver = Resolver;
10 | }
11 |
12 | loadInitializers(App, config.modulePrefix);
13 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 | [*]
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 | indent_style = space
13 | indent_size = 2
14 |
15 | [*.hbs]
16 | insert_final_newline = false
17 |
18 | [*.{diff,md}]
19 | trim_trailing_whitespace = false
20 |
--------------------------------------------------------------------------------
/tests/dummy/app/router.js:
--------------------------------------------------------------------------------
1 | import AddonDocsRouter, { docsRoute } from 'ember-cli-addon-docs/router';
2 | import config from 'dummy/config/environment';
3 |
4 | const Router = AddonDocsRouter.extend({
5 | location: config.locationType,
6 | rootURL: config.rootURL,
7 | });
8 |
9 | Router.map(function () {
10 | docsRoute(this, function () {
11 | this.route('abort');
12 | this.route('usage');
13 | });
14 |
15 | this.route('not-found', { path: '/*path' });
16 | });
17 |
18 | export default Router;
19 |
--------------------------------------------------------------------------------
/tests/test-helper.js:
--------------------------------------------------------------------------------
1 | import Application from 'dummy/app';
2 | import config from 'dummy/config/environment';
3 | import * as QUnit from 'qunit';
4 | import { setApplication } from '@ember/test-helpers';
5 | import { setup } from 'qunit-dom';
6 | import { start } from 'ember-qunit';
7 |
8 | import td from 'testdouble';
9 | import installVerifyAssertion from 'testdouble-qunit';
10 |
11 | installVerifyAssertion(QUnit, td);
12 |
13 | setApplication(Application.create(config.APP));
14 |
15 | setup(QUnit.assert);
16 |
17 | start();
18 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | name: require('./package').name,
5 | options: {
6 | autoImport: {
7 | webpack: {
8 | node: {
9 | global: true,
10 | },
11 | },
12 | },
13 | },
14 | included() {
15 | this.import(
16 | 'node_modules/abortcontroller-polyfill/dist/abortcontroller-polyfill-only.js',
17 | { prepend: true },
18 | );
19 | this._super.included.apply(this, arguments);
20 | },
21 | isDevelopingAddon() {
22 | return true;
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/tests/dummy/config/ember-cli-update.json:
--------------------------------------------------------------------------------
1 | {
2 | "schemaVersion": "1.0.0",
3 | "packages": [
4 | {
5 | "name": "ember-cli",
6 | "version": "5.12.0",
7 | "blueprints": [
8 | {
9 | "name": "addon",
10 | "outputRepo": "https://github.com/ember-cli/ember-addon-output",
11 | "codemodsSource": "ember-addon-codemods-manifest@1",
12 | "isBaseBlueprint": true,
13 | "options": [
14 | "--no-welcome",
15 | "--pnpm"
16 | ]
17 | }
18 | ]
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/testem.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | test_page: 'tests/index.html?hidepassed',
5 | disable_watching: true,
6 | launch_in_ci: ['Chrome'],
7 | launch_in_dev: ['Chrome'],
8 | browser_start_timeout: 120,
9 | browser_args: {
10 | Chrome: {
11 | ci: [
12 | // --no-sandbox is needed when running Chrome inside a container
13 | process.env.CI ? '--no-sandbox' : null,
14 | '--headless',
15 | '--disable-dev-shm-usage',
16 | '--disable-software-rasterizer',
17 | '--mute-audio',
18 | '--remote-debugging-port=0',
19 | '--window-size=1440,900',
20 | ].filter(Boolean),
21 | },
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | /dist/
3 | /tmp/
4 |
5 | # misc
6 | /.editorconfig
7 | /.ember-cli
8 | /.env*
9 | /.eslintcache
10 | /.eslintignore
11 | /.eslintrc.js
12 | /.git/
13 | /.github/
14 | /.gitignore
15 | /.prettierignore
16 | /.prettierrc.js
17 | /.stylelintignore
18 | /.stylelintrc.js
19 | /.template-lintrc.js
20 | /.travis.yml
21 | /.watchmanconfig
22 | /CONTRIBUTING.md
23 | /ember-cli-build.js
24 | /testem.js
25 | /tests/
26 | /tsconfig.declarations.json
27 | /tsconfig.json
28 | /yarn-error.log
29 | /yarn.lock
30 | .gitkeep
31 |
32 | # ember-try
33 | /.node_modules.ember-try/
34 | /npm-shrinkwrap.json.ember-try
35 | /package.json.ember-try
36 | /package-lock.json.ember-try
37 | /yarn.lock.ember-try
38 |
39 | /config/addon-docs.js
40 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How To Contribute
2 |
3 | ## Installation
4 |
5 | - `git clone `
6 | - `cd ember-ajax-fetch`
7 | - `pnpm install`
8 |
9 | ## Linting
10 |
11 | - `pnpm lint`
12 | - `pnpm lint:fix`
13 |
14 | ## Running tests
15 |
16 | - `pnpm test` – Runs the test suite on the current Ember version
17 | - `pnpm test:ember --server` – Runs the test suite in "watch mode"
18 | - `pnpm test:ember-compatibility` – Runs the test suite against multiple Ember versions
19 |
20 | ## Running the dummy application
21 |
22 | - `pnpm start`
23 | - Visit the dummy application at [http://localhost:4200](http://localhost:4200).
24 |
25 | For more information on using ember-cli, visit [https://cli.emberjs.com/release/](https://cli.emberjs.com/release/).
26 |
--------------------------------------------------------------------------------
/tests/dummy/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ember Ajax Fetch
6 |
7 |
8 |
9 | {{content-for "head"}}
10 |
11 |
12 |
13 |
14 | {{content-for "head-footer"}}
15 |
16 |
17 | {{content-for "body"}}
18 |
19 |
20 |
21 |
22 | {{content-for "body-footer"}}
23 |
24 |
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ember-ajax-fetch
2 |
3 | This addon provides a `fetch` service that is meant to have the same API as
4 | [ember-ajax](https://github.com/ember-cli/ember-ajax). It should be a drop in replacement
5 | when it is finished, and already handles a lot of things!
6 |
7 | ## Compatibility
8 |
9 | - Ember.js v3.28 or above
10 | - Ember CLI v3.28 or above
11 | - Node.js v18 or above
12 |
13 | ## Installation
14 |
15 | ```
16 | ember install ember-ajax-fetch
17 | ```
18 |
19 | ## Docs
20 |
21 | ---
22 |
23 | [View the docs](https://robbiethewagner.github.io/ember-ajax-fetch/)
24 |
25 | ## Contributing
26 |
27 | See the [Contributing](CONTRIBUTING.md) guide for details.
28 |
29 | ## License
30 |
31 | This project is licensed under the [MIT License](LICENSE.md).
32 |
--------------------------------------------------------------------------------
/ember-cli-build.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon');
4 |
5 | module.exports = function (defaults) {
6 | const app = new EmberAddon(defaults, {
7 | // Add options here
8 | autoImport: {
9 | webpack: {
10 | resolve: {
11 | fallback: {
12 | module: false,
13 | },
14 | },
15 | },
16 | },
17 | });
18 |
19 | /*
20 | This build file specifies the options for the dummy test app of this
21 | addon, located in `/tests/dummy`
22 | This build file does *not* influence how the addon or the app using it
23 | behave. You most likely want to be modifying `./index.js` or app's build file
24 | */
25 |
26 | const { maybeEmbroider } = require('@embroider/test-setup');
27 | return maybeEmbroider(app, {
28 | skipBabel: [
29 | {
30 | package: 'qunit',
31 | },
32 | ],
33 | });
34 | };
35 |
--------------------------------------------------------------------------------
/tests/dummy/config/deploy.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | 'use strict';
3 |
4 | module.exports = function (deployTarget) {
5 | let ENV = {
6 | build: {},
7 | // include other plugin configuration that applies to all deploy targets here
8 | };
9 |
10 | if (deployTarget === 'development') {
11 | ENV.build.environment = 'development';
12 | // configure other plugins for development deploy target here
13 | }
14 |
15 | if (deployTarget === 'staging') {
16 | ENV.build.environment = 'production';
17 | // configure other plugins for staging deploy target here
18 | }
19 |
20 | if (deployTarget === 'production') {
21 | ENV.build.environment = 'production';
22 | // configure other plugins for production deploy target here
23 | }
24 |
25 | // Note: if you need to build some configuration asynchronously, you can return
26 | // a promise that resolves with the ENV object instead of returning the
27 | // ENV object synchronously.
28 | return ENV;
29 | };
30 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
10 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/index.md:
--------------------------------------------------------------------------------
1 | ember-ajax-fetch
2 | ==============================================================================
3 |
4 | This addon provides a `fetch` service that is meant to have the same API as
5 | [ember-ajax](https://github.com/ember-cli/ember-ajax). It should be a drop in replacement
6 | when it is finished, and already handles a lot of things!
7 |
8 |
9 | Compatibility
10 | ------------------------------------------------------------------------------
11 |
12 | * Ember.js v3.4 or above
13 | * Ember CLI v2.13 or above
14 | * Node.js v8 or above
15 |
16 |
17 | Installation
18 | ------------------------------------------------------------------------------
19 |
20 | ```
21 | ember install ember-ajax-fetch
22 | ```
23 |
24 |
25 | Contributing
26 | ------------------------------------------------------------------------------
27 |
28 | See the [Contributing](CONTRIBUTING.md) guide for details.
29 |
30 |
31 | License
32 | ------------------------------------------------------------------------------
33 |
34 | This project is licensed under the [MIT License](LICENSE.md).
35 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/abort.md:
--------------------------------------------------------------------------------
1 | # Aborting Requests
2 |
3 | Fetch requests can be aborted by utilizing an [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
4 | They are not supported in IE11, but we have included a [polyfill](https://www.npmjs.com/package/abortcontroller-polyfill) for IE11,
5 | so it should catch the error.
6 |
7 | ## AbortController Example
8 |
9 | You will want to create a new `AbortController` and pass it in the options to the `fetch` request.
10 |
11 | ```js
12 | import Route from '@ember/routing/route';
13 | import { inject as service } from '@ember/service';
14 |
15 | export default class FooRoute extends Route {
16 | @service fetch;
17 |
18 | const queryParams = {
19 | 'filter[interval_type]': 'month'
20 | };
21 |
22 | model() {
23 | const abortController = new AbortController();
24 | return this.fetch.request('/foo/bar', {
25 | data: queryParams,
26 | signal: abortController.signal
27 | });
28 | }
29 | }
30 | ```
31 |
32 | You can then cancel a request later by using the `AbortController` you created.
33 |
34 | ```js
35 | abortController.abort();
36 | ```
37 |
--------------------------------------------------------------------------------
/tests/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Dummy Tests
6 |
7 |
8 |
9 | {{content-for "head"}}
10 | {{content-for "test-head"}}
11 |
12 |
13 |
14 |
15 |
16 | {{content-for "head-footer"}}
17 | {{content-for "test-head-footer"}}
18 |
19 |
20 | {{content-for "body"}}
21 | {{content-for "test-body"}}
22 |
23 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {{content-for "body-footer"}}
37 | {{content-for "test-body-footer"}}
38 |
39 |
40 |
--------------------------------------------------------------------------------
/tests/helpers/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | setupApplicationTest as upstreamSetupApplicationTest,
3 | setupRenderingTest as upstreamSetupRenderingTest,
4 | setupTest as upstreamSetupTest,
5 | } from 'ember-qunit';
6 |
7 | // This file exists to provide wrappers around ember-qunit's
8 | // test setup functions. This way, you can easily extend the setup that is
9 | // needed per test type.
10 |
11 | function setupApplicationTest(hooks, options) {
12 | upstreamSetupApplicationTest(hooks, options);
13 |
14 | // Additional setup for application tests can be done here.
15 | //
16 | // For example, if you need an authenticated session for each
17 | // application test, you could do:
18 | //
19 | // hooks.beforeEach(async function () {
20 | // await authenticateSession(); // ember-simple-auth
21 | // });
22 | //
23 | // This is also a good place to call test setup functions coming
24 | // from other addons:
25 | //
26 | // setupIntl(hooks, 'en-us'); // ember-intl
27 | // setupMirage(hooks); // ember-cli-mirage
28 | }
29 |
30 | function setupRenderingTest(hooks, options) {
31 | upstreamSetupRenderingTest(hooks, options);
32 |
33 | // Additional setup for rendering tests can be done here.
34 | }
35 |
36 | function setupTest(hooks, options) {
37 | upstreamSetupTest(hooks, options);
38 |
39 | // Additional setup for unit tests can be done here.
40 | }
41 |
42 | export { setupApplicationTest, setupRenderingTest, setupTest };
43 |
--------------------------------------------------------------------------------
/tests/dummy/config/environment.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function (environment) {
4 | const ENV = {
5 | modulePrefix: 'dummy',
6 | environment,
7 | rootURL: '/',
8 | locationType: 'history',
9 | EmberENV: {
10 | EXTEND_PROTOTYPES: false,
11 | FEATURES: {
12 | // Here you can enable experimental features on an ember canary build
13 | // e.g. EMBER_NATIVE_DECORATOR_SUPPORT: true
14 | },
15 | },
16 |
17 | APP: {
18 | // Here you can pass flags/options to your application instance
19 | // when it is created
20 | },
21 | };
22 |
23 | if (environment === 'development') {
24 | // ENV.APP.LOG_RESOLVER = true;
25 | // ENV.APP.LOG_ACTIVE_GENERATION = true;
26 | // ENV.APP.LOG_TRANSITIONS = true;
27 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
28 | // ENV.APP.LOG_VIEW_LOOKUPS = true;
29 | }
30 |
31 | if (environment === 'test') {
32 | // Testem prefers this...
33 | ENV.locationType = 'none';
34 |
35 | // keep test console output quieter
36 | ENV.APP.LOG_ACTIVE_GENERATION = false;
37 | ENV.APP.LOG_VIEW_LOOKUPS = false;
38 |
39 | ENV.APP.rootElement = '#ember-testing';
40 | ENV.APP.autoboot = false;
41 | }
42 |
43 | if (environment === 'production') {
44 | // Allow ember-cli-addon-docs to update the rootURL in compiled assets
45 | ENV.rootURL = 'ADDON_DOCS_ROOT_URL';
46 | // here you can enable a production-specific feature
47 | }
48 |
49 | return ENV;
50 | };
51 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/index.hbs:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{svg-jar "ember" class="docs-h-full docs-w-auto docs-max-w-full docs-fill-current" height="auto" width="125px"}}
12 |
13 |
14 | AJAX Fetch
15 |
16 |
17 |
19 | A drop-in ember-ajax replacement fetch service.
20 |
21 |
22 |
27 | Read the docs →
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/tests/unit/request-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { setupTest } from 'ember-qunit';
3 | import { isNotFoundResponse } from 'ember-fetch/errors';
4 | import Pretender from 'pretender';
5 | import request from 'ember-ajax-fetch/request';
6 |
7 | module('Unit | request', function (hooks) {
8 | setupTest(hooks);
9 |
10 | hooks.beforeEach(function () {
11 | this.server = new Pretender();
12 | });
13 |
14 | hooks.afterEach(function () {
15 | this.server.shutdown();
16 | });
17 |
18 | test('produces data', function (assert) {
19 | const photos = [
20 | { id: 10, src: 'http://media.giphy.com/media/UdqUo8xvEcvgA/giphy.gif' },
21 | { id: 42, src: 'http://media0.giphy.com/media/Ko2pyD26RdYRi/giphy.gif' },
22 | ];
23 | this.server.get('/photos', function () {
24 | return [
25 | 200,
26 | { 'Content-Type': 'application/json' },
27 | JSON.stringify(photos),
28 | ];
29 | });
30 | return request('/photos').then(function (data) {
31 | assert.deepEqual(data, photos);
32 | });
33 | });
34 |
35 | test('rejects promise when 404 is returned', function (assert) {
36 | this.server.get('/photos', function () {
37 | return [404, { 'Content-Type': 'application/json' }];
38 | });
39 |
40 | let errorCalled;
41 | return request('/photos')
42 | .then(function () {
43 | errorCalled = false;
44 | })
45 | .catch(function (response) {
46 | assert.ok(isNotFoundResponse(response));
47 | errorCalled = true;
48 | })
49 | .finally(function () {
50 | assert.ok(errorCalled);
51 | });
52 | });
53 | });
54 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = {
4 | root: true,
5 | parser: '@babel/eslint-parser',
6 | parserOptions: {
7 | ecmaVersion: 'latest',
8 | sourceType: 'module',
9 | requireConfigFile: false,
10 | babelOptions: {
11 | plugins: [
12 | ['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }],
13 | ],
14 | },
15 | },
16 | plugins: ['ember'],
17 | extends: [
18 | 'eslint:recommended',
19 | 'plugin:ember/recommended',
20 | 'plugin:prettier/recommended',
21 | ],
22 | env: {
23 | browser: true,
24 | },
25 | globals: {
26 | FastBoot: false,
27 | },
28 | rules: {
29 | 'ember/no-get': 'off',
30 | 'ember/no-jquery': 'error',
31 | 'ember/no-mixins': 'off',
32 | 'ember/no-new-mixins': 'off',
33 | 'no-console': 'off',
34 | 'no-useless-catch': 'off',
35 | },
36 | overrides: [
37 | // node files
38 | {
39 | files: [
40 | './.eslintrc.js',
41 | './.prettierrc.js',
42 | './.stylelintrc.js',
43 | './.template-lintrc.js',
44 | './ember-cli-build.js',
45 | './index.js',
46 | './testem.js',
47 | './blueprints/*/index.js',
48 | './config/**/*.js',
49 | './tests/dummy/config/**/*.js',
50 | ],
51 | parserOptions: {
52 | sourceType: 'script',
53 | },
54 | env: {
55 | browser: false,
56 | node: true,
57 | },
58 | extends: ['plugin:n/recommended'],
59 | },
60 | {
61 | // test files
62 | files: ['tests/**/*-test.{js,ts}'],
63 | extends: ['plugin:qunit/recommended'],
64 | rules: {
65 | 'qunit/no-assert-logical-expression': 'off',
66 | 'qunit/no-commented-tests': 'off',
67 | 'qunit/require-expect': 'off',
68 | },
69 | },
70 | ],
71 | };
72 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - master
8 | pull_request: {}
9 |
10 | concurrency:
11 | group: ci-${{ github.head_ref || github.ref }}
12 | cancel-in-progress: true
13 |
14 | jobs:
15 | test:
16 | name: "Tests"
17 | runs-on: ubuntu-latest
18 | timeout-minutes: 10
19 |
20 | steps:
21 | - uses: actions/checkout@v4
22 | - uses: pnpm/action-setup@v3
23 | with:
24 | version: 9
25 | - uses: actions/setup-node@v4
26 | with:
27 | node-version: 18
28 | cache: pnpm
29 | - name: Install Dependencies
30 | run: pnpm install --frozen-lockfile
31 | - name: Lint
32 | run: pnpm lint
33 | - name: Run Tests
34 | run: pnpm test
35 |
36 | floating:
37 | name: "Floating Dependencies"
38 | runs-on: ubuntu-latest
39 | timeout-minutes: 10
40 |
41 | steps:
42 | - uses: actions/checkout@v4
43 | - uses: pnpm/action-setup@v3
44 | with:
45 | version: 9
46 | - uses: actions/setup-node@v4
47 | with:
48 | node-version: 18
49 | cache: pnpm
50 | - name: Install Dependencies
51 | run: pnpm install --no-lockfile
52 | - name: Run Tests
53 | run: pnpm test
54 |
55 | try-scenarios:
56 | name: ${{ matrix.try-scenario }}
57 | runs-on: ubuntu-latest
58 | needs: "test"
59 | timeout-minutes: 10
60 |
61 | strategy:
62 | fail-fast: false
63 | matrix:
64 | try-scenario:
65 | - ember-lts-3.28
66 | - ember-lts-4.4
67 | - ember-lts-4.8
68 | - ember-lts-4.12
69 | - ember-lts-5.4
70 | - ember-release
71 | - ember-beta
72 | - ember-canary
73 | - embroider-safe
74 | - embroider-optimized
75 |
76 | steps:
77 | - uses: actions/checkout@v4
78 | - uses: pnpm/action-setup@v3
79 | with:
80 | version: 9
81 | - uses: actions/setup-node@v4
82 | with:
83 | node-version: 18
84 | cache: pnpm
85 | - name: Install Dependencies
86 | run: pnpm install --frozen-lockfile
87 | - name: Run Tests
88 | run: ./node_modules/.bin/ember try:one ${{ matrix.try-scenario }}
89 |
--------------------------------------------------------------------------------
/RELEASE.md:
--------------------------------------------------------------------------------
1 | # Release
2 |
3 | Releases are mostly automated using
4 | [release-it](https://github.com/release-it/release-it/) and
5 | [lerna-changelog](https://github.com/lerna/lerna-changelog/).
6 |
7 |
8 | ## Preparation
9 |
10 | Since the majority of the actual release process is automated, the primary
11 | remaining task prior to releasing is confirming that all pull requests that
12 | have been merged since the last release have been labeled with the appropriate
13 | `lerna-changelog` labels and the titles have been updated to ensure they
14 | represent something that would make sense to our users. Some great information
15 | on why this is important can be found at
16 | [keepachangelog.com](https://keepachangelog.com/en/1.0.0/), but the overall
17 | guiding principle here is that changelogs are for humans, not machines.
18 |
19 | When reviewing merged PR's the labels to be used are:
20 |
21 | * breaking - Used when the PR is considered a breaking change.
22 | * enhancement - Used when the PR adds a new feature or enhancement.
23 | * bug - Used when the PR fixes a bug included in a previous release.
24 | * documentation - Used when the PR adds or updates documentation.
25 | * internal - Used for internal changes that still require a mention in the
26 | changelog/release notes.
27 |
28 |
29 | ## Release
30 |
31 | Once the prep work is completed, the actual release is straight forward:
32 |
33 | * First, ensure that you have installed your projects dependencies:
34 |
35 | ```
36 | npm install
37 | ```
38 |
39 | * Second, ensure that you have obtained a
40 | [GitHub personal access token][generate-token] with the `repo` scope (no
41 | other permissions are needed). Make sure the token is available as the
42 | `GITHUB_AUTH` environment variable.
43 |
44 | For instance:
45 |
46 | ```bash
47 | export GITHUB_AUTH=abc123def456
48 | ```
49 |
50 | [generate-token]: https://github.com/settings/tokens/new?scopes=repo&description=GITHUB_AUTH+env+variable
51 |
52 | * And last (but not least 😁) do your release.
53 |
54 | ```
55 | npx release-it
56 | ```
57 |
58 | [release-it](https://github.com/release-it/release-it/) manages the actual
59 | release process. It will prompt you to to choose the version number after which
60 | you will have the chance to hand tweak the changelog to be used (for the
61 | `CHANGELOG.md` and GitHub release), then `release-it` continues on to tagging,
62 | pushing the tag and commits, etc.
63 |
--------------------------------------------------------------------------------
/addon/errors.js:
--------------------------------------------------------------------------------
1 | export class FetchError extends Error {
2 | constructor(payload, message = 'Ajax operation failed', status) {
3 | super(message);
4 |
5 | this.payload = payload;
6 | this.status = status;
7 | }
8 | }
9 |
10 | export class InvalidError extends FetchError {
11 | constructor(payload) {
12 | super(payload, 'Request was rejected because it was invalid', 422);
13 | }
14 | }
15 |
16 | export class UnauthorizedError extends FetchError {
17 | constructor(payload) {
18 | super(payload, 'Ajax authorization failed', 401);
19 | }
20 | }
21 |
22 | export class ForbiddenError extends FetchError {
23 | constructor(payload) {
24 | super(
25 | payload,
26 | 'Request was rejected because user is not permitted to perform this operation.',
27 | 403,
28 | );
29 | }
30 | }
31 |
32 | export class BadRequestError extends FetchError {
33 | constructor(payload) {
34 | super(payload, 'Request was formatted incorrectly.', 400);
35 | }
36 | }
37 |
38 | export class NotFoundError extends FetchError {
39 | constructor(payload) {
40 | super(payload, 'Resource was not found.', 404);
41 | }
42 | }
43 |
44 | export class GoneError extends FetchError {
45 | constructor(payload) {
46 | super(payload, 'Resource is no longer available.', 410);
47 | }
48 | }
49 |
50 | export class TimeoutError extends FetchError {
51 | constructor() {
52 | super(null, 'The ajax operation timed out', -1);
53 | }
54 | }
55 |
56 | export class AbortError extends FetchError {
57 | constructor() {
58 | super(null, 'The ajax operation was aborted', 0);
59 |
60 | this.name = 'AbortError';
61 | }
62 | }
63 |
64 | export class ConflictError extends FetchError {
65 | constructor(payload) {
66 | super(payload, 'The ajax operation failed due to a conflict', 409);
67 | }
68 | }
69 |
70 | export class ServerError extends FetchError {
71 | constructor(payload, status) {
72 | super(payload, 'Request was rejected due to server error', status);
73 | }
74 | }
75 |
76 | /**
77 | * Checks if the given error is or inherits from FetchError
78 | * @function isFetchError
79 | */
80 | export function isFetchError(error) {
81 | return error instanceof FetchError;
82 | }
83 |
84 | /**
85 | * Checks if the given object represents a "timeout" error
86 | * @function isTimeoutError
87 | */
88 | export function isTimeoutError(error) {
89 | return error instanceof TimeoutError;
90 | }
91 |
--------------------------------------------------------------------------------
/tests/dummy/config/ember-try.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const getChannelURL = require('ember-source-channel-url');
4 | const { embroiderSafe, embroiderOptimized } = require('@embroider/test-setup');
5 |
6 | module.exports = async function () {
7 | return {
8 | usePnpm: true,
9 | buildManagerOptions() {
10 | return ['--ignore-scripts', '--no-frozen-lockfile'];
11 | },
12 | scenarios: [
13 | {
14 | name: 'ember-lts-3.28',
15 | npm: {
16 | devDependencies: {
17 | '@ember/test-helpers': '^2.5.0',
18 | 'ember-cli': '^3.28.0',
19 | 'ember-data': '^3.28.0',
20 | 'ember-qunit': '^6.0.0',
21 | 'ember-resolver': '^8.0.0',
22 | 'ember-source': '~3.28.6',
23 | },
24 | },
25 | },
26 | {
27 | name: 'ember-lts-4.4',
28 | npm: {
29 | devDependencies: {
30 | 'ember-resolver': '^11.0.1',
31 | 'ember-source': '~4.4.0',
32 | },
33 | },
34 | },
35 | {
36 | name: 'ember-lts-4.8',
37 | npm: {
38 | devDependencies: {
39 | 'ember-resolver': '^11.0.1',
40 | 'ember-source': '~4.8.0',
41 | },
42 | },
43 | },
44 | {
45 | name: 'ember-lts-4.12',
46 | npm: {
47 | devDependencies: {
48 | 'ember-source': '~4.12.0',
49 | },
50 | },
51 | },
52 | {
53 | name: 'ember-lts-5.4',
54 | npm: {
55 | devDependencies: {
56 | 'ember-source': '~5.4.0',
57 | },
58 | },
59 | },
60 | {
61 | name: 'ember-release',
62 | npm: {
63 | devDependencies: {
64 | 'ember-source': await getChannelURL('release'),
65 | },
66 | },
67 | },
68 | {
69 | name: 'ember-beta',
70 | npm: {
71 | devDependencies: {
72 | 'ember-cli': '~3.28.0',
73 | 'ember-source': await getChannelURL('beta'),
74 | },
75 | },
76 | },
77 | {
78 | name: 'ember-canary',
79 | npm: {
80 | devDependencies: {
81 | 'ember-cli': '~3.28.0',
82 | 'ember-source': await getChannelURL('canary'),
83 | },
84 | },
85 | },
86 | embroiderSafe(),
87 | embroiderOptimized(),
88 | ],
89 | };
90 | };
91 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ## v2.1.0 (2025-03-26)
5 |
6 | #### :rocket: Enhancement
7 | * [#63](https://github.com/RobbieTheWagner/ember-ajax-fetch/pull/63) Add support for compressed types response ([@batrasaurabh90](https://github.com/batrasaurabh90))
8 |
9 | #### Committers: 1
10 | - Saurabh Batra ([@batrasaurabh90](https://github.com/batrasaurabh90))
11 |
12 | ## v2.0.1 (2025-02-19)
13 |
14 | #### :bug: Bug Fix
15 | * [#60](https://github.com/RobbieTheWagner/ember-ajax-fetch/pull/60) Return text response ([@damatri](https://github.com/damatri))
16 |
17 | #### Committers: 1
18 | - [@damatri](https://github.com/damatri)
19 |
20 | ## v2.0.0 (2025-02-18)
21 |
22 | #### :boom: Breaking Change
23 | * [#62](https://github.com/RobbieTheWagner/ember-ajax-fetch/pull/62) Require Ember 3.28+, node >= 18 ([@RobbieTheWagner](https://github.com/RobbieTheWagner))
24 |
25 | #### :rocket: Enhancement
26 | * [#59](https://github.com/RobbieTheWagner/ember-ajax-fetch/pull/59) Remove deprecated `EmberError` and `assign` ([@wozny1989](https://github.com/wozny1989))
27 |
28 | #### Committers: 8
29 | - AJ McClure ([@audiocipher](https://github.com/audiocipher))
30 | - Adam Woźny ([@wozny1989](https://github.com/wozny1989))
31 | - Andrew Regan ([@andrew-regan-expel](https://github.com/andrew-regan-expel))
32 | - Camille TJHOA ([@ctjhoa](https://github.com/ctjhoa))
33 | - Daren McCulley ([@mcculleydj](https://github.com/mcculleydj))
34 | - Matthew Blasius ([@slickmb](https://github.com/slickmb))
35 | - Nabeel Zafar ([@nabeelz7](https://github.com/nabeelz7))
36 | - Robbie Wagner ([@RobbieTheWagner](https://github.com/RobbieTheWagner))
37 |
38 | ## v1.0.3 (2021-05-20)
39 |
40 | #### :house: Internal
41 | * [#22](https://github.com/expel-io/ember-ajax-fetch/pull/22) Use GitHub actions for CI ([@rwwagner90](https://github.com/rwwagner90))
42 |
43 | #### Committers: 2
44 | - Robert Wagner ([@rwwagner90](https://github.com/rwwagner90))
45 | - Scott Fisk ([@scottfisk](https://github.com/scottfisk))
46 |
47 | ## v1.0.2 (2020-12-04)
48 |
49 |
50 | ## v1.0.1 (2020-10-27)
51 |
52 | #### :house: Internal
53 | * [#19](https://github.com/expel-io/ember-ajax-fetch/pull/19) Fix security issues, update ember-fetch and move to deps ([@rwwagner90](https://github.com/rwwagner90))
54 | * [#18](https://github.com/expel-io/ember-ajax-fetch/pull/18) Convert to yarn ([@rwwagner90](https://github.com/rwwagner90))
55 |
56 | #### Committers: 2
57 | - Robert Wagner ([@rwwagner90](https://github.com/rwwagner90))
58 | - Roger Studner ([@rstudner](https://github.com/rstudner))
59 |
60 |
61 | ## v1.0.0 (2020-10-23)
62 |
63 | #### :house: Internal
64 | * [#14](https://github.com/expel-io/ember-ajax-fetch/pull/14) Add rwjblue release-it ([@rwwagner90](https://github.com/rwwagner90))
65 |
66 | #### Committers: 1
67 | - Robert Wagner ([@rwwagner90](https://github.com/rwwagner90))
68 |
69 |
--------------------------------------------------------------------------------
/addon/-private/utils/json-helpers.js:
--------------------------------------------------------------------------------
1 | import { COMPRESSED_TYPES } from '../constants/response';
2 |
3 | /**
4 | * Determine if a string is JSON or not
5 | * @param {string} str The string to check for JSON formatting
6 | * @return {boolean}
7 | * @function isJsonString
8 | * @private
9 | */
10 | export function isJsonString(str) {
11 | try {
12 | const json = JSON.parse(str);
13 | return typeof json === 'object';
14 | } catch (e) {
15 | return false;
16 | }
17 | }
18 |
19 | /**
20 | * Parses the JSON returned by a network request
21 | *
22 | * @param {object} response A response from a network request
23 | * @return {object} The parsed JSON, status from the response
24 | * @function parseJSON
25 | * @private
26 | */
27 | export async function parseJSON(response) {
28 | const responseType =
29 | response.headers.get('content-type') || 'Empty Content-Type';
30 | let error = {
31 | status: response.status,
32 | statusText: response.statusText,
33 | };
34 |
35 | if (!response.ok) {
36 | const errorBody = await response.text();
37 | error.message = errorBody;
38 | }
39 |
40 | return new Promise((resolve) => {
41 | if (responseType.includes('json')) {
42 | return response
43 | .json()
44 | .then((json) => {
45 | if (response.ok) {
46 | return resolve({
47 | status: response.status,
48 | ok: response.ok,
49 | json,
50 | });
51 | } else {
52 | error = Object.assign({}, json, error);
53 |
54 | return resolve(error);
55 | }
56 | })
57 | .catch((err) => {
58 | if (isJsonString(error.message)) {
59 | error.payload = JSON.parse(error.message);
60 | } else {
61 | error.payload = error.message || err.toString();
62 | }
63 |
64 | error.message = error.message || err.toString();
65 |
66 | return resolve(error);
67 | });
68 | } else if (COMPRESSED_TYPES.includes(responseType)) {
69 | return response
70 | .blob()
71 | .then((blob) => {
72 | return resolve({
73 | status: response.status,
74 | ok: response.ok,
75 | blob,
76 | });
77 | })
78 | .catch((err) => {
79 | handleError(error, err);
80 |
81 | return resolve(error);
82 | });
83 | } else {
84 | return response
85 | .text()
86 | .then((text) => {
87 | return resolve({
88 | status: response.status,
89 | ok: response.ok,
90 | text,
91 | });
92 | })
93 | .catch((err) => {
94 | handleError(error, err);
95 |
96 | return resolve(error);
97 | });
98 | }
99 | });
100 | }
101 |
102 | // Helper function to handle JSON parsing errors
103 | function handleError(error, err) {
104 | if (isJsonString(error.message)) {
105 | error.payload = JSON.parse(error.message);
106 | } else {
107 | error.payload = error.message || err.toString();
108 | }
109 | error.message = error.message || err.toString();
110 | }
111 |
--------------------------------------------------------------------------------
/addon/-private/utils/url-helpers.js:
--------------------------------------------------------------------------------
1 | const completeUrlRegex = /^(http|https)/;
2 |
3 | /**
4 | * Parse a URL string into an object that defines its structure
5 | *
6 | * The returned object will have the following properties:
7 | *
8 | * href: the full URL
9 | * protocol: the request protocol
10 | * hostname: the target for the request
11 | * port: the port for the request
12 | * pathname: any URL after the host
13 | * search: query parameters
14 | * hash: the URL hash
15 | *
16 | * @param {string} str
17 | * @function parseURL
18 | * @private
19 | */
20 | export function parseURL(str) {
21 | let fullObject;
22 |
23 | if (typeof FastBoot === 'undefined') {
24 | const element = document.createElement('a');
25 | element.href = str;
26 | fullObject = element;
27 | } else {
28 | fullObject = FastBoot.require('url').parse(str);
29 | }
30 |
31 | const desiredProps = {
32 | href: fullObject.href,
33 | protocol: fullObject.protocol,
34 | hostname: fullObject.hostname,
35 | port: fullObject.port,
36 | pathname: fullObject.pathname,
37 | search: fullObject.search,
38 | hash: fullObject.hash,
39 | };
40 |
41 | return desiredProps;
42 | }
43 |
44 | /**
45 | * Returns true if both `a` and `b` have the same protocol, hostname, and port.
46 | * @param {string} a
47 | * @param {string} b
48 | * @return {boolean}
49 | * @function haveSameHost
50 | * @private
51 | */
52 | export function haveSameHost(a, b) {
53 | const urlA = parseURL(a);
54 | const urlB = parseURL(b);
55 |
56 | return (
57 | urlA.protocol === urlB.protocol &&
58 | urlA.hostname === urlB.hostname &&
59 | urlA.port === urlB.port
60 | );
61 | }
62 |
63 | /**
64 | * Checks if the URL is already a full URL
65 | * @param {string} url
66 | * @return {boolean}
67 | * @function isFullURL
68 | * @private
69 | */
70 | export function isFullURL(url) {
71 | return !!url.match(completeUrlRegex);
72 | }
73 |
74 | /**
75 | * Checks if the given string starts with '/'
76 | * @param {string} string The string to check
77 | * @return {boolean}
78 | * @function startsWithSlash
79 | * @private
80 | */
81 | export function startsWithSlash(string) {
82 | return string.charAt(0) === '/';
83 | }
84 |
85 | /**
86 | * Checks if the given string ends with '/'
87 | * @param {string} string The string to check
88 | * @return {boolean}
89 | * @function endsWithSlash
90 | * @private
91 | */
92 | export function endsWithSlash(string) {
93 | return string.charAt(string.length - 1) === '/';
94 | }
95 |
96 | /**
97 | * Remove a leading slash from the given string
98 | * @param {string} string The string to remove the slash from
99 | * @return {string}
100 | * @function removeLeadingSlash
101 | * @private
102 | */
103 | export function removeLeadingSlash(string) {
104 | return string.substring(1);
105 | }
106 |
107 | /**
108 | * Remove a trailing slash from the given string
109 | * @param {string} string The string to remove the slash from
110 | * @return {string}
111 | * @function removeTrailingSlash
112 | * @private
113 | */
114 | export function removeTrailingSlash(string) {
115 | return string.slice(0, -1);
116 | }
117 |
118 | /**
119 | * Strip slashes from the given path
120 | * @param {string} path The path to remove slashes from
121 | * @return {string}
122 | * @function stripSlashes
123 | * @private
124 | */
125 | export function stripSlashes(path) {
126 | // make sure path starts with `/`
127 | if (startsWithSlash(path)) {
128 | path = removeLeadingSlash(path);
129 | }
130 |
131 | // remove end `/`
132 | if (endsWithSlash(path)) {
133 | path = removeTrailingSlash(path);
134 | }
135 | return path;
136 | }
137 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ember-ajax-fetch",
3 | "version": "2.1.0",
4 | "description": "A drop-in ember-ajax replacement fetch service.",
5 | "keywords": [
6 | "ember-addon"
7 | ],
8 | "homepage": "https://expel-io.github.io/ember-ajax-fetch",
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/expel-io/ember-ajax-fetch.git"
12 | },
13 | "license": "MIT",
14 | "author": "",
15 | "directories": {
16 | "doc": "doc",
17 | "test": "tests"
18 | },
19 | "scripts": {
20 | "build": "ember build --environment=production",
21 | "lint": "concurrently \"pnpm:lint:*(!fix)\" --names \"lint:\"",
22 | "lint:css": "stylelint \"**/*.css\"",
23 | "lint:css:fix": "concurrently \"pnpm:lint:css -- --fix\"",
24 | "lint:fix": "concurrently \"pnpm:lint:*:fix\" --names \"fix:\"",
25 | "lint:hbs": "ember-template-lint .",
26 | "lint:hbs:fix": "ember-template-lint . --fix",
27 | "lint:js": "eslint . --cache",
28 | "lint:js:fix": "eslint . --fix",
29 | "start": "ember serve",
30 | "test": "concurrently \"pnpm:lint\" \"pnpm:test:*\" --names \"lint,test:\"",
31 | "test:ember": "ember test"
32 | },
33 | "dependencies": {
34 | "@babel/core": "^7.25.2",
35 | "abortcontroller-polyfill": "^1.7.8",
36 | "ember-auto-import": "^2.10.0",
37 | "ember-cli-babel": "^8.2.0",
38 | "ember-cli-htmlbars": "^6.3.0",
39 | "ember-fetch": "^8.1.2",
40 | "jquery-param": "^1.2.4"
41 | },
42 | "devDependencies": {
43 | "@babel/eslint-parser": "^7.26.8",
44 | "@babel/plugin-proposal-decorators": "^7.25.9",
45 | "@ember/optional-features": "^2.2.0",
46 | "@ember/string": "^3.0.1",
47 | "@ember/test-helpers": "^3.3.1",
48 | "@embroider/test-setup": "^4.0.0",
49 | "@glimmer/component": "^1.1.2",
50 | "@glimmer/tracking": "^1.1.2",
51 | "@release-it-plugins/lerna-changelog": "^6.1.0",
52 | "base-64": "^1.0.0",
53 | "broccoli-asset-rev": "^3.0.0",
54 | "concurrently": "^8.2.2",
55 | "ember-cli": "~5.12.0",
56 | "ember-cli-addon-docs": "^4.2.2",
57 | "ember-cli-addon-docs-yuidoc": "^1.1.0",
58 | "ember-cli-clean-css": "^3.0.0",
59 | "ember-cli-dependency-checker": "^3.3.3",
60 | "ember-cli-deploy": "^1.0.2",
61 | "ember-cli-deploy-build": "^2.0.0",
62 | "ember-cli-deploy-git": "^1.3.4",
63 | "ember-cli-deploy-git-ci": "^1.0.1",
64 | "ember-cli-inject-live-reload": "^2.1.0",
65 | "ember-cli-sri": "^2.1.1",
66 | "ember-cli-terser": "^4.0.2",
67 | "ember-data": "^4.12.8",
68 | "ember-load-initializers": "^2.1.2",
69 | "ember-page-title": "^8.2.3",
70 | "ember-qunit": "^8.1.0",
71 | "ember-resolver": "^12.0.1",
72 | "ember-source": "^5.12.0",
73 | "ember-source-channel-url": "^3.0.0",
74 | "ember-template-lint": "^6.0.0",
75 | "ember-try": "^3.0.0",
76 | "eslint": "^8.57.1",
77 | "eslint-config-prettier": "^9.1.0",
78 | "eslint-plugin-ember": "^12.2.1",
79 | "eslint-plugin-n": "^16.6.2",
80 | "eslint-plugin-prettier": "^5.2.1",
81 | "eslint-plugin-qunit": "^8.1.2",
82 | "loader.js": "^4.7.0",
83 | "pretender": "^3.4.7",
84 | "prettier": "^3.3.3",
85 | "qunit": "^2.24.1",
86 | "qunit-dom": "^3.2.1",
87 | "release-it": "^17.11.0",
88 | "stylelint": "^15.11.0",
89 | "stylelint-config-standard": "^34.0.0",
90 | "stylelint-prettier": "^4.1.0",
91 | "testdouble": "^3.20.2",
92 | "testdouble-qunit": "^2.1.1",
93 | "webpack": "^5.98.0"
94 | },
95 | "peerDependencies": {
96 | "ember-source": "^3.28.0 || >= 4.0.0"
97 | },
98 | "engines": {
99 | "node": ">= 18"
100 | },
101 | "publishConfig": {
102 | "registry": "https://registry.npmjs.org"
103 | },
104 | "ember": {
105 | "edition": "octane"
106 | },
107 | "ember-addon": {
108 | "configPath": "tests/dummy/config"
109 | },
110 | "release-it": {
111 | "plugins": {
112 | "@release-it-plugins/lerna-changelog": {
113 | "infile": "CHANGELOG.md",
114 | "launchEditor": true
115 | }
116 | },
117 | "git": {
118 | "tagName": "v${version}"
119 | },
120 | "github": {
121 | "release": true,
122 | "tokenRef": "GITHUB_AUTH"
123 | }
124 | }
125 | }
126 |
--------------------------------------------------------------------------------
/tests/dummy/app/templates/docs/usage.md:
--------------------------------------------------------------------------------
1 | # Usage
2 |
3 | ## Basic Usage
4 |
5 | In general, you will use the `request(url, options)` method, where url is the destination of the request and options is a
6 | configuration hash compatible with `ember-ajax`, which will be transformed to use with `fetch`.
7 |
8 | ```js
9 | import Route from '@ember/routing/route';
10 | import { inject as service } from '@ember/service';
11 |
12 | export default class FooRoute extends Route {
13 | @service fetch;
14 |
15 | const queryParams = {
16 | 'filter[interval_type]': 'month'
17 | };
18 |
19 | model() {
20 | return this.fetch.request('/foo/bar', {
21 | data: queryParams
22 | });
23 | }
24 | }
25 | ```
26 |
27 | ### HTTP-verbed methods
28 |
29 | You can skip setting the `method` or `type` keys in your `options` object when
30 | calling `request(url, options)` by instead calling `post(url, options)`,
31 | `put(url, options)`, `patch(url, options)` or `del(url, options)`.
32 |
33 | ```js
34 | post('/posts', { data: { title: 'Ember' } }); // Makes a POST request to /posts
35 | put('/posts/1', { data: { title: 'Ember' } }); // Makes a PUT request to /posts/1
36 | patch('/posts/1', { data: { title: 'Ember' } }); // Makes a PATCH request to /posts/1
37 | del('/posts/1'); // Makes a DELETE request to /posts/1
38 | ```
39 |
40 | ### Custom Request Headers
41 |
42 | `ember-ajax-fetch` allows you to specify headers to be used with a request. This is
43 | especially helpful when you have a session service that provides an auth token
44 | that you have to include with the requests to authorize your requests.
45 |
46 | To include custom headers to be used with your requests, you can specify
47 | `headers` hash on the `Fetch Service`.
48 |
49 | ```js
50 | // app/services/fetch.js
51 |
52 | import { computed } from '@ember/object';
53 | import { inject as service } from '@ember/service';
54 | import FetchService from 'ember-ajax-fetch/services/fetch';
55 |
56 | export default class ExtendedFetchService extends FetchService {
57 | @service session;
58 |
59 | @computed('session.authToken')
60 | get headers() {
61 | let headers = {};
62 | const authToken = this.get('session.authToken');
63 | if (authToken) {
64 | headers['auth-token'] = authToken;
65 | }
66 | return headers;
67 | }
68 | }
69 | ```
70 |
71 | Headers by default are only passed if the hosts match, or the request is a relative path.
72 | You can overwrite this behavior by either passing a host in with the request, setting the
73 | host for the fetch service, or by setting an array of `trustedHosts` that can be either
74 | an array of strings or regexes.
75 |
76 | ```js
77 | // app/services/fetch.js
78 |
79 | import FetchService from 'ember-ajax-fetch/services/fetch';
80 |
81 | export default class ExtendedFetchService extends FetchService {
82 | trustedHosts = [/\.example\./, 'foo.bar.com'];
83 | }
84 | ```
85 |
86 | ### Custom Endpoint Path
87 |
88 | The `namespace` property can be used to prefix requests with a specific url namespace.
89 |
90 | ```js
91 | // app/services/fetch.js
92 |
93 | import FetchService from 'ember-ajax-fetch/services/fetch';
94 |
95 | export default class ExtendedFetchService extends FetchService {
96 | namespace = '/api/v1';
97 | }
98 | ```
99 |
100 | `request('/users/me')` would now target `/api/v1/users/me`
101 |
102 | If you need to override the namespace for a custom request, use the `namespace` as an option to the request methods.
103 |
104 | ```js
105 | // GET /api/legacy/users/me
106 | request('/users/me', { namespace: '/api/legacy' });
107 | ```
108 |
109 | ### Custom Host
110 |
111 | `ember-ajax-fetch` allows you to specify a host to be used with a request. This is
112 | especially helpful so you don't have to continually pass in the host along
113 | with the path, makes `request()` a bit cleaner.
114 |
115 | To include a custom host to be used with your requests, you can specify `host`
116 | property on the `Fetch Service`.
117 |
118 | ```js
119 | // app/services/fetch.js
120 |
121 | import FetchService from 'ember-ajax-fetch/services/fetch';
122 |
123 | export default class ExtendedFetchService extends FetchService {
124 | host = 'http://api.example.com';
125 | }
126 | ```
127 |
128 | That allows you to only have to make a call to `request()` as such:
129 |
130 | ```js
131 | // GET http://api.example.com/users/me
132 | request('/users/me');
133 | ```
134 |
135 | ### Custom Content-Type
136 |
137 | `ember-ajax-fetch` allows you to specify a default [Content-Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) header to be used with a request.
138 |
139 | To include a custom Content-Type you can specify `contentType` property on the `Fetch Service`.
140 |
141 | ```js
142 | // app/services/fetch.js
143 |
144 | import FetchService from 'ember-ajax-fetch/services/fetch';
145 |
146 | export default class ExtendedFetchService extends FetchService {
147 | contentType = 'application/json; charset=utf-8';
148 | }
149 | ```
150 |
151 | You can also override the Content-Type per `request` with the `options` parameter.
152 |
153 | ### Error handling
154 |
155 | `ember-ajax-fetch` provides built in error classes that you can use to check the error
156 | that was returned by the response. This allows you to restrict determination of
157 | error result to the service instead of sprinkling it around your code.
158 |
159 | #### Built in error types
160 |
161 | `ember-ajax-fetch` has built-in error types that will be returned from the service in the event of an error:
162 |
163 | - `BadRequestError` (400)
164 | - `UnauthorizedError`(401)
165 | - `ForbiddenError`(403)
166 | - `NotFoundError` (404)
167 | - `InvalidError`(422)
168 | - `ServerError` (5XX)
169 | - `AbortError`
170 | - `TimeoutError`
171 |
172 | All of the above errors are subtypes of `FetchError`.
173 |
174 | #### Error detection helpers
175 |
176 | `ember-ajax-fetch` uses the helper functions from `ember-fetch` for matching response errors to their respective `ember-ajax-fetch` error type.
177 | Each of the errors listed above has a corresponding `is*` function (e.g., `isBadRequestResponse`), which can be imported from
178 | `ember-fetch/errors`
179 |
180 | Use of these functions is **strongly encouraged** to help eliminate the need for boilerplate error detection code.
181 |
182 | ```js
183 | import Route from '@ember/routing/route';
184 | import { inject as service } from '@ember/service';
185 | import {
186 | isNotFoundResponse,
187 | isForbiddenResponse
188 | } from 'ember-fetch/errors';
189 | import { isFetchError } from 'ember-ajax-fetch/errors';
190 |
191 | export default Route.extend({
192 | fetch: service(),
193 | model() {
194 | return this.fetch.request('/user/doesnotexist').catch(function(error) {
195 | if (isNotFoundResponse(error)) {
196 | // handle 404 errors here
197 | return;
198 | }
199 |
200 | if (isForbiddenResponse(error)) {
201 | // handle 403 errors here
202 | return;
203 | }
204 |
205 | if (isFetchError(error)) {
206 | // handle all other AjaxErrors here
207 | return;
208 | }
209 |
210 | // other errors are handled elsewhere
211 | throw error;
212 | });
213 | }
214 | });
215 | ```
216 |
217 | If your errors aren't standard, the helper function for that error type can be used as the base to build your custom detection function.
218 |
219 | ## Options
220 |
221 | ### timeout
222 |
223 | Timeouts are not supported by `fetch` by default, but using an `AbortController`, we
224 | implemented support for them, so it's the same API as AJAX.
225 |
226 | ```js
227 | this.fetch.request('/foo/bar', {
228 | timeout: 500
229 | });
230 | ```
231 |
--------------------------------------------------------------------------------
/tests/unit/errors-test.js:
--------------------------------------------------------------------------------
1 | import { module, test } from 'qunit';
2 | import { setupTest } from 'ember-qunit';
3 | import {
4 | isAbortError,
5 | isBadRequestResponse,
6 | isConflictResponse,
7 | isForbiddenResponse,
8 | isGoneResponse,
9 | isInvalidResponse,
10 | isNotFoundResponse,
11 | isServerErrorResponse,
12 | isUnauthorizedResponse,
13 | } from 'ember-fetch/errors';
14 | import {
15 | FetchError,
16 | InvalidError,
17 | UnauthorizedError,
18 | ForbiddenError,
19 | NotFoundError,
20 | GoneError,
21 | BadRequestError,
22 | ServerError,
23 | TimeoutError,
24 | AbortError,
25 | ConflictError,
26 | isFetchError,
27 | } from 'ember-ajax-fetch/errors';
28 |
29 | module('Unit | Errors Test', function (hooks) {
30 | setupTest(hooks);
31 |
32 | test('FetchError', function (assert) {
33 | const error = new FetchError();
34 | assert.ok(error instanceof Error);
35 | });
36 |
37 | test('InvalidError', function (assert) {
38 | const error = new InvalidError();
39 | assert.ok(error instanceof Error);
40 | assert.ok(error instanceof InvalidError);
41 | });
42 |
43 | test('UnauthorizedError', function (assert) {
44 | const error = new UnauthorizedError();
45 | assert.ok(error instanceof Error);
46 | assert.ok(error instanceof UnauthorizedError);
47 | });
48 |
49 | test('ForbiddenError', function (assert) {
50 | const error = new ForbiddenError();
51 | assert.ok(error instanceof Error);
52 | assert.ok(error instanceof ForbiddenError);
53 | });
54 |
55 | test('NotFoundError', function (assert) {
56 | const error = new NotFoundError();
57 | assert.ok(error instanceof Error);
58 | assert.ok(error instanceof NotFoundError);
59 | });
60 |
61 | test('GoneError', function (assert) {
62 | const error = new GoneError();
63 | assert.ok(error instanceof Error);
64 | assert.ok(error instanceof GoneError);
65 | });
66 |
67 | test('BadRequestError', function (assert) {
68 | const error = new BadRequestError();
69 | assert.ok(error instanceof Error);
70 | assert.ok(error instanceof BadRequestError);
71 | });
72 |
73 | test('ServerError', function (assert) {
74 | const error = new ServerError();
75 | assert.ok(error instanceof Error);
76 | assert.ok(error instanceof ServerError);
77 | });
78 |
79 | test('TimeoutError', function (assert) {
80 | const error = new TimeoutError();
81 | assert.ok(error instanceof Error);
82 | assert.ok(error instanceof TimeoutError);
83 | });
84 |
85 | test('AbortError', function (assert) {
86 | const error = new AbortError();
87 | assert.ok(error instanceof Error);
88 | assert.ok(error instanceof AbortError);
89 | });
90 |
91 | test('ConflictError', function (assert) {
92 | const error = new ConflictError();
93 | assert.ok(error instanceof Error);
94 | assert.ok(error instanceof ConflictError);
95 | });
96 |
97 | module('isUnauthorizedResponse', function () {
98 | test('detects error code correctly', function (assert) {
99 | assert.ok(isUnauthorizedResponse({ status: 401 }));
100 | });
101 |
102 | test('detects error class correctly', function (assert) {
103 | const error = new UnauthorizedError();
104 | assert.ok(isUnauthorizedResponse(error));
105 | });
106 | });
107 |
108 | module('isForbiddenResponse', function () {
109 | test('detects error code correctly', function (assert) {
110 | assert.ok(isForbiddenResponse({ status: 403 }));
111 | });
112 |
113 | test('detects error class correctly', function (assert) {
114 | const error = new ForbiddenError();
115 | assert.ok(isForbiddenResponse(error));
116 | });
117 | });
118 |
119 | module('isNotFoundResponse', function () {
120 | test(': detects error code correctly', function (assert) {
121 | assert.ok(isNotFoundResponse({ status: 404 }));
122 | assert.notOk(isNotFoundResponse({ status: 400 }));
123 | });
124 |
125 | test('detects error class correctly', function (assert) {
126 | const error = new NotFoundError();
127 | const otherError = new Error();
128 | assert.ok(isNotFoundResponse(error));
129 | assert.notOk(isNotFoundResponse(otherError));
130 | });
131 | });
132 |
133 | module('isGoneResponse', function () {
134 | test(': detects error code correctly', function (assert) {
135 | assert.ok(isGoneResponse({ status: 410 }));
136 | assert.notOk(isGoneResponse({ status: 400 }));
137 | });
138 |
139 | test('detects error class correctly', function (assert) {
140 | const error = new GoneError();
141 | const otherError = new Error();
142 | assert.ok(isGoneResponse(error));
143 | assert.notOk(isGoneResponse(otherError));
144 | });
145 | });
146 |
147 | module('isInvalidResponse', function () {
148 | test('detects error code correctly', function (assert) {
149 | assert.ok(isInvalidResponse({ status: 422 }));
150 | });
151 |
152 | test('detects error class correctly', function (assert) {
153 | const error = new InvalidError();
154 | assert.ok(isInvalidResponse(error));
155 | });
156 | });
157 |
158 | module('isBadRequestResponse', function () {
159 | test('detects error code correctly', function (assert) {
160 | assert.ok(isBadRequestResponse({ status: 400 }));
161 | });
162 |
163 | test('detects error class correctly', function (assert) {
164 | const error = new BadRequestError();
165 | assert.ok(isBadRequestResponse(error));
166 | });
167 | });
168 |
169 | module('isServerErrorResponse', function () {
170 | test('detects error code correctly', function (assert) {
171 | assert.notOk(
172 | isServerErrorResponse({ status: 499 }),
173 | '499 is not a server error',
174 | );
175 | assert.ok(
176 | isServerErrorResponse({ status: 500 }),
177 | '500 is a server error',
178 | );
179 | assert.ok(
180 | isServerErrorResponse({ status: 599 }),
181 | '599 is a server error',
182 | );
183 | assert.notOk(
184 | isServerErrorResponse({ status: 600 }),
185 | '600 is not a server error',
186 | );
187 | });
188 | });
189 |
190 | module('isFetchError', function () {
191 | test('detects error class correctly', function (assert) {
192 | const ajaxError = new FetchError();
193 | const notAjaxError = new Error();
194 | const ajaxErrorSubtype = new BadRequestError();
195 | assert.ok(isFetchError(ajaxError));
196 | assert.notOk(isFetchError(notAjaxError));
197 | assert.ok(isFetchError(ajaxErrorSubtype));
198 | });
199 | });
200 |
201 | module('isAbortError', function () {
202 | test('detects error class correctly', function (assert) {
203 | const error = new AbortError();
204 | assert.ok(isAbortError(error));
205 | });
206 | });
207 |
208 | module('isConflictResponse', function () {
209 | test('detects error code correctly', function (assert) {
210 | assert.ok(isConflictResponse({ status: 409 }));
211 | });
212 | });
213 | });
214 |
215 | // describe('unit/errors-test - FetchError', function() {
216 | //
217 | // test('isTimeoutError: detects error class correctly', function() {
218 | // const error = new TimeoutError();
219 | // assert.ok(isTimeoutError(error));
220 | // });
221 | //
222 | // describe('isSuccess', function() {
223 | // test('detects successful request correctly', function() {
224 | // notOk(isSuccess(100));
225 | // notOk(isSuccess(199));
226 | // assert.ok(isSuccess(200));
227 | // assert.ok(isSuccess(299));
228 | // notOk(isSuccess(300));
229 | // assert.ok(isSuccess(304));
230 | // notOk(isSuccess(400));
231 | // notOk(isSuccess(500));
232 | // });
233 | // });
234 | // });
235 |
--------------------------------------------------------------------------------
/addon/mixins/fetch-request.js:
--------------------------------------------------------------------------------
1 | import { A } from '@ember/array';
2 | import Mixin from '@ember/object/mixin';
3 | import { get } from '@ember/object';
4 | import { isEmpty } from '@ember/utils';
5 | import fetch from 'fetch';
6 | import param from 'jquery-param';
7 | import {
8 | isAbortError,
9 | isBadRequestResponse,
10 | isConflictResponse,
11 | isForbiddenResponse,
12 | isGoneResponse,
13 | isInvalidResponse,
14 | isNotFoundResponse,
15 | isServerErrorResponse,
16 | isUnauthorizedResponse,
17 | } from 'ember-fetch/errors';
18 | import {
19 | FetchError,
20 | UnauthorizedError,
21 | InvalidError,
22 | ForbiddenError,
23 | BadRequestError,
24 | NotFoundError,
25 | GoneError,
26 | AbortError,
27 | ConflictError,
28 | ServerError,
29 | } from 'ember-ajax-fetch/errors';
30 | import {
31 | endsWithSlash,
32 | haveSameHost,
33 | isFullURL,
34 | parseURL,
35 | removeLeadingSlash,
36 | removeTrailingSlash,
37 | startsWithSlash,
38 | stripSlashes,
39 | } from 'ember-ajax-fetch/-private/utils/url-helpers';
40 | import isString from 'ember-ajax-fetch/-private/utils/is-string';
41 | import {
42 | isJsonString,
43 | parseJSON,
44 | } from 'ember-ajax-fetch/-private/utils/json-helpers';
45 |
46 | /**
47 | * @class FetchRequestMixin
48 | */
49 | export default Mixin.create({
50 | /**
51 | * The default value for the request `contentType`
52 | *
53 | * For now, defaults to the same value that jQuery would assign. In the
54 | * future, the default value will be for JSON requests.
55 | * @property {string} contentType
56 | * @public
57 | */
58 | contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
59 |
60 | /**
61 | * Make a fetch request, returning the raw fetch response
62 | *
63 | * Unlike ember-ajax this method returns the raw fetch response not an xhr
64 | * @method request
65 | * @param {string} url The url for the request
66 | * @param {object} options The options hash for the request
67 | * @return {object} containing {response, requestOptions, builtURL}
68 | */
69 | async raw(url, options = {}) {
70 | const hash = this.options(url, options);
71 | const method = hash.method || hash.type || 'GET';
72 | const requestOptions = {
73 | method,
74 | headers: {
75 | ...(hash.headers || {}),
76 | },
77 | };
78 |
79 | const abortController = new AbortController();
80 | requestOptions.signal = abortController.signal;
81 |
82 | // If `contentType` is set to false, we want to not send anything and let the browser decide
83 | // We also want to ensure that no content-type was manually set on options.headers before overwriting it
84 | if (
85 | options.contentType !== false &&
86 | isEmpty(requestOptions.headers['Content-Type'])
87 | ) {
88 | requestOptions.headers['Content-Type'] = hash.contentType;
89 | }
90 |
91 | let builtURL = hash.url;
92 | if (hash.data) {
93 | let { data } = hash;
94 |
95 | if (options.processData === false) {
96 | requestOptions.body = data;
97 | } else {
98 | if (isJsonString(data)) {
99 | data = JSON.parse(data);
100 | }
101 |
102 | if (requestOptions.method === 'GET') {
103 | builtURL = `${builtURL}?${param(data)}`;
104 | } else {
105 | requestOptions.body = JSON.stringify(data);
106 | }
107 | }
108 | }
109 |
110 | try {
111 | // Used to manually pass another AbortController signal in, for external aborting
112 | if (options.signal) {
113 | options.signal.addEventListener('abort', () => abortController.abort());
114 | }
115 | let timeout;
116 | if (options.timeout) {
117 | timeout = setTimeout(() => abortController.abort(), options.timeout);
118 | }
119 | let response = await fetch(builtURL, requestOptions);
120 | if (timeout) {
121 | clearTimeout(timeout);
122 | }
123 |
124 | return { response, requestOptions, builtURL };
125 | } catch (error) {
126 | // TODO: do we want to just throw here or should some errors be okay?
127 | throw error;
128 | }
129 | },
130 |
131 | /**
132 | * Make a fetch request, ignoring the raw fetch response and dealing only with
133 | * the response content
134 | * @method request
135 | * @param {string} url The url for the request
136 | * @param {object} options The options hash for the request
137 | * @return {Promise<*>}
138 | */
139 | async request(url, options = {}) {
140 | let { response, requestOptions, builtURL } = await this.raw(url, options);
141 | response = await parseJSON(response);
142 |
143 | return this._handleResponse(response, requestOptions, builtURL);
144 | },
145 |
146 | /**
147 | * Determine whether the headers should be added for this request
148 | *
149 | * This hook is used to help prevent sending headers to every host, regardless
150 | * of the destination, since this could be a security issue if authentication
151 | * tokens are accidentally leaked to third parties.
152 | *
153 | * To avoid that problem, subclasses should utilize the `headers` computed
154 | * property to prevent authentication from being sent to third parties, or
155 | * implement this hook for more fine-grain control over when headers are sent.
156 | *
157 | * By default, the headers are sent if the host of the request matches the
158 | * `host` property designated on the class.
159 | *
160 | * @method _shouldSendHeaders
161 | * @return {boolean|*}
162 | * @private
163 | */
164 | _shouldSendHeaders({ url, host }) {
165 | url = url || '';
166 | host = host || get(this, 'host') || '';
167 |
168 | const trustedHosts = get(this, 'trustedHosts') || A();
169 | const { hostname } = parseURL(url);
170 |
171 | // Add headers on relative URLs
172 | if (!isFullURL(url)) {
173 | return true;
174 | } else if (
175 | trustedHosts.find((matcher) => this._matchHosts(hostname, matcher))
176 | ) {
177 | return true;
178 | }
179 |
180 | // Add headers on matching host
181 | return haveSameHost(url, host);
182 | },
183 |
184 | /**
185 | * Generates a detailed ("friendly") error message, with plenty
186 | * of information for debugging (good luck!)
187 | */
188 | generateDetailedMessage(status, payload, contentType, type, url) {
189 | let shortenedPayload;
190 | const payloadContentType = contentType || 'Empty Content-Type';
191 |
192 | if (
193 | payloadContentType.toLowerCase() === 'text/html' &&
194 | payload.length > 250
195 | ) {
196 | shortenedPayload = '[Omitted Lengthy HTML]';
197 | } else {
198 | shortenedPayload = JSON.stringify(payload);
199 | }
200 |
201 | const requestDescription = `${type} ${url}`;
202 | const payloadDescription = `Payload (${payloadContentType})`;
203 |
204 | return [
205 | `Ember Ajax Fetch Request ${requestDescription} returned a ${status}`,
206 | payloadDescription,
207 | shortenedPayload,
208 | ].join('\n');
209 | },
210 |
211 | /**
212 | * Created a normalized set of options from the per-request and
213 | * service-level settings
214 | * @method options
215 | * @param {string} url The url for the request
216 | * @param {object} options The options hash for the request
217 | * @return {object}
218 | */
219 | options(url, options = {}) {
220 | options = Object.assign({}, options);
221 | options.url = this._buildURL(url, options);
222 | options.type = options.type || 'GET';
223 | options.dataType = options.dataType || 'json';
224 | options.contentType = isEmpty(options.contentType)
225 | ? get(this, 'contentType')
226 | : options.contentType;
227 |
228 | if (this._shouldSendHeaders(options)) {
229 | options.headers = this._getFullHeadersHash(options.headers);
230 | } else {
231 | options.headers = options.headers || {};
232 | }
233 |
234 | return options;
235 | },
236 |
237 | /**
238 | * Build the URL to pass to `fetch`
239 | * @method _buildURL
240 | * @param {string} url The base url
241 | * @param {object} options The options to pass to fetch, query params, headers, etc
242 | * @return {string} The built url
243 | * @private
244 | */
245 | _buildURL(url, options = {}) {
246 | if (isFullURL(url)) {
247 | return url;
248 | }
249 |
250 | const urlParts = [];
251 |
252 | let host = options.host || get(this, 'host');
253 | if (host) {
254 | host = endsWithSlash(host) ? removeTrailingSlash(host) : host;
255 | urlParts.push(host);
256 | }
257 |
258 | let namespace = options.namespace || get(this, 'namespace');
259 | if (namespace) {
260 | // If host is given then we need to strip leading slash too( as it will be added through join)
261 | if (host) {
262 | namespace = stripSlashes(namespace);
263 | } else if (endsWithSlash(namespace)) {
264 | namespace = removeTrailingSlash(namespace);
265 | }
266 |
267 | const hasNamespaceRegex = new RegExp(`^(/)?${stripSlashes(namespace)}/`);
268 | if (!hasNamespaceRegex.test(url)) {
269 | urlParts.push(namespace);
270 | }
271 | }
272 |
273 | // *Only* remove a leading slash -- we need to maintain a trailing slash for
274 | // APIs that differentiate between it being and not being present
275 | if (startsWithSlash(url) && urlParts.length !== 0) {
276 | url = removeLeadingSlash(url);
277 | }
278 | urlParts.push(url);
279 |
280 | return urlParts.join('/');
281 | },
282 |
283 | /**
284 | * Return the correct error type
285 | * @method _createCorrectError
286 | * @param {object} response The response from the fetch call
287 | * @param {*} payload The response.json() payload
288 | * @param {object} requestOptions The options object containing headers, method, etc
289 | * @param {string} url The url string
290 | * @private
291 | */
292 | _createCorrectError(response, payload, requestOptions, url) {
293 | let error;
294 |
295 | if (isUnauthorizedResponse(response)) {
296 | error = new UnauthorizedError(payload);
297 | } else if (isForbiddenResponse(response)) {
298 | error = new ForbiddenError(payload);
299 | } else if (isInvalidResponse(response)) {
300 | error = new InvalidError(payload);
301 | } else if (isBadRequestResponse(response)) {
302 | error = new BadRequestError(payload);
303 | } else if (isNotFoundResponse(response)) {
304 | error = new NotFoundError(payload);
305 | } else if (isGoneResponse(response)) {
306 | error = new GoneError(payload);
307 | } else if (isAbortError(response)) {
308 | error = new AbortError();
309 | } else if (isConflictResponse(response)) {
310 | error = new ConflictError(payload);
311 | } else if (isServerErrorResponse(response)) {
312 | error = new ServerError(payload, response.status);
313 | } else {
314 | const detailedMessage = this.generateDetailedMessage(
315 | response.status,
316 | payload,
317 | requestOptions.headers['Content-Type'],
318 | requestOptions.method,
319 | url,
320 | );
321 |
322 | error = new FetchError(payload, detailedMessage, response.status);
323 | }
324 |
325 | return error;
326 | },
327 |
328 | /**
329 | * Calls `request()` but forces `options.type` to `POST`
330 | * @method post
331 | * @param {string} url The url for the request
332 | * @param {object} options The options object for the request
333 | * @return {*|Promise<*>}
334 | */
335 | post(url, options) {
336 | return this.request(url, this._addTypeToOptionsFor(options, 'POST'));
337 | },
338 |
339 | /**
340 | * Calls `request()` but forces `options.type` to `PUT`
341 | * @method put
342 | * @param {string} url The url for the request
343 | * @param {object} options The options object for the request
344 | * @return {*|Promise<*>}
345 | */
346 | put(url, options) {
347 | return this.request(url, this._addTypeToOptionsFor(options, 'PUT'));
348 | },
349 |
350 | /**
351 | * Calls `request()` but forces `options.type` to `PATCH`
352 | * @method patch
353 | * @param {string} url The url for the request
354 | * @param {object} options The options object for the request
355 | * @return {*|Promise<*>}
356 | */
357 | patch(url, options) {
358 | return this.request(url, this._addTypeToOptionsFor(options, 'PATCH'));
359 | },
360 |
361 | /**
362 | * Calls `request()` but forces `options.type` to `DELETE`
363 | * @method del
364 | * @param {string} url The url for the request
365 | * @param {object} options The options object for the request
366 | * @return {*|Promise<*>}
367 | */
368 | del(url, options) {
369 | return this.request(url, this._addTypeToOptionsFor(options, 'DELETE'));
370 | },
371 |
372 | /**
373 | * Calls `request()` but forces `options.type` to `DELETE`
374 | *
375 | * Alias for `del()`
376 | * @method delete
377 | * @param {string} url The url for the request
378 | * @param {object} options The options object for the request
379 | * @return {*|Promise<*>}
380 | */
381 | delete(url, options) {
382 | return this.del(url, options);
383 | },
384 |
385 | /**
386 | * Wrap the `.get` method so that we issue a warning if
387 | *
388 | * Since `.get` is both an AJAX pattern _and_ an Ember pattern, we want to try
389 | * to warn users when they try using `.get` to make a request
390 | * @param {string} url
391 | * @return {*}
392 | */
393 | get(url) {
394 | if (arguments.length > 1 || url.indexOf('/') !== -1) {
395 | throw new Error(
396 | 'It seems you tried to use `.get` to make a request! Use the `.request` method instead.',
397 | );
398 | }
399 | return this._super(...arguments);
400 | },
401 |
402 | /**
403 | * Manipulates the options hash to include the HTTP method on the type key
404 | * @method _addTypeOptionsFor
405 | * @param {object} options The request options hash
406 | * @param {string} method The type of request. GET, POST, etc
407 | * @return {*|{}}
408 | * @private
409 | */
410 | _addTypeToOptionsFor(options, method) {
411 | options = options || {};
412 | options.type = method;
413 | return options;
414 | },
415 |
416 | /**
417 | * Get the full "headers" hash, combining the service-defined headers with
418 | * the ones provided for the request
419 | * @method _getFullHeadersHash
420 | * @param {object} headers The headers passed in the options hash
421 | * @private
422 | */
423 | _getFullHeadersHash(headers) {
424 | const classHeaders = get(this, 'headers');
425 | return Object.assign({}, classHeaders, headers);
426 | },
427 |
428 | /**
429 | * Return the response or handle the error
430 | * @method _handleResponse
431 | * @param {object} response The response from the request
432 | * @param {object} requestOptions The options object containing headers, method, etc
433 | * @param {string} url The url for the request
434 | * @return {*}
435 | * @private
436 | */
437 | _handleResponse(response, requestOptions, url) {
438 | if (response.ok) {
439 | return response.text || response.json || response.blob;
440 | } else {
441 | throw this._createCorrectError(
442 | response,
443 | response.payload,
444 | requestOptions,
445 | url,
446 | );
447 | }
448 | },
449 |
450 | /**
451 | * Match the host to a provided array of strings or regexes that can match to a host
452 | * @method _matchHosts
453 | * @param {string|undefined} host
454 | * @param {string} matcher
455 | * @private
456 | */
457 | _matchHosts(host, matcher) {
458 | if (!isString(host)) {
459 | return false;
460 | }
461 |
462 | if (matcher instanceof RegExp) {
463 | return matcher.test(host);
464 | } else if (typeof matcher === 'string') {
465 | return matcher === host;
466 | } else {
467 | console.warn(
468 | 'trustedHosts only handles strings or regexes. ',
469 | matcher,
470 | ' is neither.',
471 | );
472 | return false;
473 | }
474 | },
475 | });
476 |
--------------------------------------------------------------------------------
/tests/unit/mixins/fetch-request-test.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable ember/avoid-leaking-state-in-ember-objects */
2 | import { A } from '@ember/array';
3 | import { typeOf } from '@ember/utils';
4 | import { encode as base64Encode } from 'base-64';
5 | import FetchRequest from 'ember-ajax-fetch/fetch-request';
6 | import Pretender from 'pretender';
7 | import { module, test } from 'qunit';
8 | import { setupTest } from 'ember-qunit';
9 | import td from 'testdouble';
10 | import { jsonFactory, jsonResponse } from 'dummy/tests/helpers/json';
11 | // import { isTimeoutError } from 'ember-ajax-fetch/errors';
12 | import {
13 | UnauthorizedError,
14 | InvalidError,
15 | ForbiddenError,
16 | BadRequestError,
17 | GoneError,
18 | ConflictError,
19 | ServerError,
20 | } from 'ember-ajax-fetch/errors';
21 |
22 | const {
23 | matchers: { anything, contains: matchContains },
24 | } = td;
25 |
26 | module('Unit | Mixin | fetch-request', function (hooks) {
27 | setupTest(hooks);
28 |
29 | hooks.beforeEach(function () {
30 | this.server = new Pretender();
31 | });
32 |
33 | hooks.afterEach(function () {
34 | this.server.shutdown();
35 | });
36 |
37 | module('options method', function () {
38 | test('sets raw data', function (assert) {
39 | const service = FetchRequest.create();
40 | const url = '/test';
41 | const type = 'GET';
42 | const options = service.options(url, {
43 | type,
44 | data: { key: 'value' },
45 | });
46 |
47 | assert.deepEqual(options, {
48 | contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
49 | data: {
50 | key: 'value',
51 | },
52 | dataType: 'json',
53 | headers: {},
54 | type: 'GET',
55 | url: '/test',
56 | });
57 | });
58 |
59 | test('sets options correctly', function (assert) {
60 | const service = FetchRequest.create();
61 | const url = '/test';
62 | const type = 'POST';
63 | const data = JSON.stringify({ key: 'value' });
64 | const options = service.options(url, {
65 | type,
66 | data,
67 | contentType: 'application/json; charset=utf-8',
68 | });
69 |
70 | assert.deepEqual(options, {
71 | contentType: 'application/json; charset=utf-8',
72 | data: '{"key":"value"}',
73 | dataType: 'json',
74 | headers: {},
75 | type: 'POST',
76 | url: '/test',
77 | });
78 | });
79 |
80 | test('does not modify the options object argument', function (assert) {
81 | const service = FetchRequest.create();
82 | const url = 'test';
83 | const data = JSON.stringify({ key: 'value' });
84 | const baseOptions = { type: 'POST', data };
85 | service.options(url, baseOptions);
86 | assert.deepEqual(baseOptions, { type: 'POST', data });
87 | });
88 |
89 | test('does not override contentType when defined', function (assert) {
90 | const service = FetchRequest.create();
91 | const url = '/test';
92 | const type = 'POST';
93 | const data = JSON.stringify({ key: 'value' });
94 | const options = service.options(url, {
95 | type,
96 | data,
97 | contentType: false,
98 | });
99 |
100 | assert.deepEqual(options, {
101 | contentType: false,
102 | data: '{"key":"value"}',
103 | dataType: 'json',
104 | headers: {},
105 | type: 'POST',
106 | url: '/test',
107 | });
108 | });
109 |
110 | test('can handle empty data', function (assert) {
111 | const service = FetchRequest.create();
112 | const url = '/test';
113 | const type = 'POST';
114 | const options = service.options(url, { type });
115 |
116 | assert.deepEqual(options, {
117 | contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
118 | dataType: 'json',
119 | headers: {},
120 | type: 'POST',
121 | url: '/test',
122 | });
123 | });
124 |
125 | test('is only called once per call to request', function (assert) {
126 | let numberOptionsCalls = 0;
127 |
128 | this.server.get('/foo', () => jsonResponse());
129 |
130 | const MonitorOptionsCalls = FetchRequest.extend({
131 | options() {
132 | numberOptionsCalls = numberOptionsCalls + 1;
133 | return this._super(...arguments);
134 | },
135 | });
136 |
137 | const service = MonitorOptionsCalls.create();
138 | return service.request('/foo').then(function () {
139 | assert.strictEqual(numberOptionsCalls, 1);
140 | });
141 | });
142 |
143 | module('host', function () {
144 | test('is set on the url (url starting with `/`)', function (assert) {
145 | const RequestWithHost = FetchRequest.extend({
146 | host: 'https://discuss.emberjs.com',
147 | });
148 |
149 | const service = RequestWithHost.create();
150 | const url = '/users/me';
151 | const options = service.options(url);
152 |
153 | assert.strictEqual(options.url, 'https://discuss.emberjs.com/users/me');
154 | });
155 |
156 | test('is set on the url (url not starting with `/`)', function (assert) {
157 | const RequestWithHost = FetchRequest.extend({
158 | host: 'https://discuss.emberjs.com',
159 | });
160 |
161 | const service = RequestWithHost.create();
162 | const url = 'users/me';
163 | const options = service.options(url);
164 |
165 | assert.strictEqual(options.url, 'https://discuss.emberjs.com/users/me');
166 | });
167 |
168 | test('is overridable on a per-request basis', function (assert) {
169 | const RequestWithHost = FetchRequest.extend({
170 | host: 'https://discuss.emberjs.com',
171 | });
172 |
173 | const service = RequestWithHost.create();
174 | const url = 'users/me';
175 | const host = 'https://myurl.com';
176 | const options = service.options(url, { host });
177 |
178 | assert.strictEqual(options.url, 'https://myurl.com/users/me');
179 | });
180 |
181 | test('is set on the namespace(namespace not starting with `/`)', function (assert) {
182 | const RequestWithHostAndNamespace = FetchRequest.extend({
183 | host: 'https://discuss.emberjs.com',
184 | namespace: 'api/v1',
185 | });
186 | const service = RequestWithHostAndNamespace.create();
187 | const url = 'users/me';
188 | const options = service.options(url);
189 |
190 | assert.strictEqual(
191 | options.url,
192 | 'https://discuss.emberjs.com/api/v1/users/me',
193 | );
194 | });
195 |
196 | test('is set on the namespace(namespace starting with `/`)', function (assert) {
197 | const RequestWithHostAndNamespace = FetchRequest.extend({
198 | host: 'https://discuss.emberjs.com',
199 | namespace: '/api/v1',
200 | });
201 | const service = RequestWithHostAndNamespace.create();
202 | const url = 'users/me';
203 | const options = service.options(url);
204 |
205 | assert.strictEqual(
206 | options.url,
207 | 'https://discuss.emberjs.com/api/v1/users/me',
208 | );
209 | });
210 |
211 | test('is set on the url containing namespace', function (assert) {
212 | const RequestWithHostAndNamespace = FetchRequest.extend({
213 | host: 'https://discuss.emberjs.com',
214 | namespace: '/api/v1',
215 | });
216 | const service = RequestWithHostAndNamespace.create();
217 |
218 | assert.strictEqual(
219 | service.options('/api/v1/users/me').url,
220 | 'https://discuss.emberjs.com/api/v1/users/me',
221 | );
222 | assert.strictEqual(
223 | service.options('api/v1/users/me').url,
224 | 'https://discuss.emberjs.com/api/v1/users/me',
225 | );
226 | });
227 |
228 | test('is set on the url containing namespace no leading slash', function (assert) {
229 | const RequestWithHostAndNamespace = FetchRequest.extend({
230 | host: 'https://discuss.emberjs.com',
231 | namespace: 'api/v1',
232 | });
233 | const service = RequestWithHostAndNamespace.create();
234 |
235 | assert.strictEqual(
236 | service.options('/api/v1/users/me').url,
237 | 'https://discuss.emberjs.com/api/v1/users/me',
238 | );
239 | assert.strictEqual(
240 | service.options('api/v1/users/me').url,
241 | 'https://discuss.emberjs.com/api/v1/users/me',
242 | );
243 | });
244 |
245 | test('is set with the host address as `//` and url not starting with `/`', function (assert) {
246 | const RequestWithHostAndNamespace = FetchRequest.extend({
247 | host: '//',
248 | });
249 | const service = RequestWithHostAndNamespace.create();
250 | const url = 'users/me';
251 | const options = service.options(url);
252 |
253 | assert.strictEqual(options.url, '//users/me');
254 | });
255 |
256 | test('is set with the host address as `//` and url starting with `/`', function (assert) {
257 | const RequestWithHostAndNamespace = FetchRequest.extend({
258 | host: '//',
259 | });
260 | const service = RequestWithHostAndNamespace.create();
261 | const url = '/users/me';
262 | const options = service.options(url);
263 |
264 | assert.strictEqual(options.url, '//users/me');
265 | });
266 | });
267 |
268 | module('namespace', function () {
269 | test('is set on the url (namespace starting with `/`)', function (assert) {
270 | const RequestWithHost = FetchRequest.extend({
271 | namespace: '/api/v1',
272 | });
273 |
274 | const service = RequestWithHost.create();
275 |
276 | assert.strictEqual(
277 | service.options('/users/me').url,
278 | '/api/v1/users/me',
279 | );
280 | assert.strictEqual(service.options('users/me').url, '/api/v1/users/me');
281 | });
282 |
283 | test('can be set on a per-request basis', function (assert) {
284 | const service = FetchRequest.create();
285 |
286 | assert.strictEqual(
287 | service.options('users/me', { namespace: '/api' }).url,
288 | '/api/users/me',
289 | );
290 | assert.strictEqual(
291 | service.options('users/me', { namespace: 'api' }).url,
292 | 'api/users/me',
293 | );
294 | });
295 |
296 | test('is set on the url (namespace not starting with `/`)', function (assert) {
297 | const RequestWithHost = FetchRequest.extend({
298 | namespace: 'api/v1',
299 | });
300 |
301 | const service = RequestWithHost.create();
302 |
303 | assert.strictEqual(service.options('/users/me').url, 'api/v1/users/me');
304 | assert.strictEqual(service.options('users/me').url, 'api/v1/users/me');
305 | });
306 | });
307 |
308 | module('type', function () {
309 | test('defaults to GET', function (assert) {
310 | const service = FetchRequest.create();
311 | const url = 'test';
312 | const options = service.options(url);
313 |
314 | assert.strictEqual(options.type, 'GET');
315 | });
316 | });
317 | });
318 |
319 | test('can override the default `contentType` for the service', function (assert) {
320 | const defaultContentType = 'application/json';
321 |
322 | class FetchServiceWithDefaultContentType extends FetchRequest {
323 | get contentType() {
324 | return defaultContentType;
325 | }
326 | }
327 |
328 | const service = FetchServiceWithDefaultContentType.create();
329 | const options = service.options('');
330 | assert.strictEqual(options.contentType, defaultContentType);
331 | });
332 |
333 | test('raw() response.post === options.data.post', function (assert) {
334 | const service = FetchRequest.create();
335 | const url = '/posts';
336 | const title = 'Title';
337 | const description = 'Some description.';
338 | const contentType = 'application/json';
339 | const customHeader = 'My custom header';
340 | const options = {
341 | data: {
342 | post: { title, description },
343 | },
344 | };
345 | const serverResponse = [
346 | 200,
347 | { 'Content-Type': contentType, 'Custom-Header': customHeader },
348 | JSON.stringify(options.data),
349 | ];
350 |
351 | this.server.get(url, () => serverResponse);
352 |
353 | const rawPromise = service.raw(url, options);
354 |
355 | return rawPromise
356 | .then(function ({ response }) {
357 | assert.strictEqual(response.status, 200);
358 | assert.strictEqual(response.headers.get('Custom-Header'), customHeader);
359 | assert.strictEqual(response.headers.get('Content-Type'), contentType);
360 | return response.json();
361 | })
362 | .then((json) => {
363 | assert.deepEqual(json.post, options.data.post);
364 | });
365 | });
366 |
367 | test('post() response.post === options.data.post', function (assert) {
368 | const service = FetchRequest.create();
369 | const url = '/posts';
370 | const title = 'Title';
371 | const description = 'Some description.';
372 | const options = {
373 | data: {
374 | post: { title, description },
375 | },
376 | };
377 | const serverResponse = [
378 | 200,
379 | { 'Content-Type': 'application/json' },
380 | JSON.stringify(options.data),
381 | ];
382 |
383 | this.server.post(url, () => serverResponse);
384 |
385 | const postPromise = service.post(url, options);
386 |
387 | return postPromise.then(function (response) {
388 | assert.deepEqual(response.post, options.data.post);
389 | });
390 | });
391 |
392 | test('post() application/zip response returns Blob', function (assert) {
393 | const service = FetchRequest.create();
394 | const url = '/posts';
395 | const dummyZipData = new Uint8Array([80, 75, 3, 4]);
396 | const base64Zip = base64Encode(String.fromCharCode(...dummyZipData));
397 | const serverResponse = [
398 | 200,
399 | { 'Content-Type': 'application/zip' },
400 | base64Zip,
401 | ];
402 |
403 | this.server.post(url, () => serverResponse);
404 |
405 | const postPromise = service.post(url, {});
406 |
407 | return postPromise.then(function (response) {
408 | assert.ok(response instanceof Blob, 'Response is an ArrayBuffer');
409 | });
410 | });
411 |
412 | test('put() response.post === options.data.post', function (assert) {
413 | const service = FetchRequest.create();
414 | const url = '/posts/1';
415 | const title = 'Title';
416 | const description = 'Some description.';
417 | const id = 1;
418 | const options = {
419 | data: {
420 | post: { id, title, description },
421 | },
422 | };
423 |
424 | const serverResponse = [
425 | 200,
426 | { 'Content-Type': 'application/json' },
427 | JSON.stringify(options.data),
428 | ];
429 |
430 | this.server.put(url, () => serverResponse);
431 |
432 | const putPromise = service.put(url, options);
433 |
434 | return putPromise.then(function (response) {
435 | assert.deepEqual(response.post, options.data.post);
436 | });
437 | });
438 |
439 | test('patch() response.post === options.data.post', function (assert) {
440 | const service = FetchRequest.create();
441 | const url = '/posts/1';
442 | const description = 'Some description.';
443 | const options = {
444 | data: {
445 | post: { description },
446 | },
447 | };
448 |
449 | const serverResponse = [
450 | 200,
451 | { 'Content-Type': 'application/json' },
452 | JSON.stringify(options.data),
453 | ];
454 |
455 | this.server.patch(url, () => serverResponse);
456 |
457 | const patchPromise = service.patch(url, options);
458 |
459 | return patchPromise.then(function (response) {
460 | assert.deepEqual(response.post, options.data.post);
461 | });
462 | });
463 |
464 | test('del() response is {}', function (assert) {
465 | const service = FetchRequest.create();
466 | const url = '/posts/1';
467 | const serverResponse = [
468 | 200,
469 | { 'Content-Type': 'application/json' },
470 | JSON.stringify({}),
471 | ];
472 |
473 | this.server.delete(url, () => serverResponse);
474 |
475 | const delPromise = service.del(url);
476 |
477 | return delPromise.then(function (response) {
478 | assert.deepEqual(response, {});
479 | });
480 | });
481 |
482 | test('delete() response is {}', function (assert) {
483 | const service = FetchRequest.create();
484 | const url = '/posts/1';
485 | const serverResponse = [
486 | 200,
487 | { 'Content-Type': 'application/json' },
488 | JSON.stringify({}),
489 | ];
490 |
491 | this.server.delete(url, () => serverResponse);
492 |
493 | const deletePromise = service.delete(url);
494 |
495 | return deletePromise.then(function (response) {
496 | assert.deepEqual(response, {});
497 | });
498 | });
499 |
500 | test('request with method option makes the correct type of request', function (assert) {
501 | const url = '/posts/1';
502 | const serverResponse = [
503 | 200,
504 | { 'Content-Type': 'application/json' },
505 | JSON.stringify({}),
506 | ];
507 |
508 | this.server.get(url, () => {
509 | throw new Error("Shouldn't make an AJAX request");
510 | });
511 | this.server.post(url, () => serverResponse);
512 |
513 | const service = FetchRequest.create();
514 | const _handleResponse = td.function('handle response');
515 | const expectedArguments = [
516 | anything(),
517 | matchContains({ method: 'POST' }),
518 | anything(),
519 | ];
520 | service._handleResponse = _handleResponse;
521 | td.when(_handleResponse(...expectedArguments)).thenReturn({});
522 |
523 | return service.request(url, { method: 'POST' }).then(() => {
524 | assert.verify(_handleResponse(...expectedArguments));
525 | });
526 | });
527 |
528 | module('explicit host in URL', function () {
529 | test('overrides host property of class', function (assert) {
530 | const RequestWithHost = FetchRequest.extend({
531 | host: 'https://discuss.emberjs.com',
532 | });
533 |
534 | const service = RequestWithHost.create();
535 | const url = 'http://myurl.com/users/me';
536 | const options = service.options(url);
537 |
538 | assert.strictEqual(options.url, 'http://myurl.com/users/me');
539 | });
540 |
541 | test('overrides host property in request config', function (assert) {
542 | const service = FetchRequest.create();
543 | const host = 'https://discuss.emberjs.com';
544 | const url = 'http://myurl.com/users/me';
545 | const options = service.options(url, { host });
546 |
547 | assert.strictEqual(options.url, 'http://myurl.com/users/me');
548 | });
549 |
550 | test('without a protocol does not override config property', function (assert) {
551 | const RequestWithHost = FetchRequest.extend({
552 | host: 'https://discuss.emberjs.com',
553 | });
554 |
555 | const service = RequestWithHost.create();
556 | const url = 'myurl.com/users/me';
557 | const options = service.options(url);
558 |
559 | assert.strictEqual(
560 | options.url,
561 | 'https://discuss.emberjs.com/myurl.com/users/me',
562 | );
563 | });
564 | });
565 |
566 | module('headers', function () {
567 | test('is set if the URL matches the host', function (assert) {
568 | this.server.get('http://example.com/test', (req) => {
569 | const { requestHeaders } = req;
570 | assert.strictEqual(requestHeaders['Content-Type'], 'application/json');
571 | assert.strictEqual(requestHeaders['Other-key'], 'Other Value');
572 | return jsonResponse();
573 | });
574 |
575 | const RequestWithHeaders = FetchRequest.extend({
576 | host: 'http://example.com',
577 | headers: {
578 | 'Content-Type': 'application/json',
579 | 'Other-key': 'Other Value',
580 | },
581 | });
582 |
583 | const service = RequestWithHeaders.create();
584 | return service.request('http://example.com/test');
585 | });
586 |
587 | test('is set if the URL is relative', function (assert) {
588 | this.server.get('/some/relative/url', (req) => {
589 | const { requestHeaders } = req;
590 | assert.strictEqual(requestHeaders['Content-Type'], 'application/json');
591 | assert.strictEqual(requestHeaders['Other-key'], 'Other Value');
592 | return jsonResponse();
593 | });
594 |
595 | const RequestWithHeaders = FetchRequest.extend({
596 | headers: {
597 | 'Content-Type': 'application/json',
598 | 'Other-key': 'Other Value',
599 | },
600 | });
601 |
602 | const service = RequestWithHeaders.create();
603 | return service.request('/some/relative/url');
604 | });
605 |
606 | test('is set if the URL matches one of the RegExp trustedHosts', function (assert) {
607 | this.server.get('http://my.example.com', (req) => {
608 | const { requestHeaders } = req;
609 | assert.strictEqual(requestHeaders['Other-key'], 'Other Value');
610 | return jsonResponse();
611 | });
612 |
613 | const RequestWithHeaders = FetchRequest.extend({
614 | host: 'some-other-host.com',
615 | trustedHosts: A([4, 'notmy.example.com', /example\./]),
616 | headers: {
617 | 'Content-Type': 'application/json',
618 | 'Other-key': 'Other Value',
619 | },
620 | });
621 |
622 | const service = RequestWithHeaders.create();
623 | return service.request('http://my.example.com');
624 | });
625 |
626 | test('is set if the URL matches one of the string trustedHosts', function (assert) {
627 | this.server.get('http://foo.bar.com', (req) => {
628 | const { requestHeaders } = req;
629 | assert.strictEqual(requestHeaders['Other-key'], 'Other Value');
630 | return jsonResponse();
631 | });
632 |
633 | const RequestWithHeaders = FetchRequest.extend({
634 | host: 'some-other-host.com',
635 | trustedHosts: A(['notmy.example.com', /example\./, 'foo.bar.com']),
636 | headers: {
637 | 'Content-Type': 'application/json',
638 | 'Other-key': 'Other Value',
639 | },
640 | });
641 |
642 | const service = RequestWithHeaders.create();
643 | return service.request('http://foo.bar.com');
644 | });
645 |
646 | test('is not set if the URL does not match the host', function (assert) {
647 | this.server.get('http://example.com', (req) => {
648 | const { requestHeaders } = req;
649 | assert.notEqual(requestHeaders['Other-key'], 'Other Value');
650 | return jsonResponse();
651 | });
652 |
653 | const RequestWithHeaders = FetchRequest.extend({
654 | host: 'some-other-host.com',
655 | headers: {
656 | 'Content-Type': 'application/json',
657 | 'Other-key': 'Other Value',
658 | },
659 | });
660 |
661 | const service = RequestWithHeaders.create();
662 | return service.request('http://example.com');
663 | });
664 |
665 | test('can be supplied on a per-request basis', function (assert) {
666 | this.server.get('http://example.com', (req) => {
667 | const { requestHeaders } = req;
668 | assert.strictEqual(requestHeaders['Per-Request-Key'], 'Some value');
669 | assert.strictEqual(requestHeaders['Other-key'], 'Other Value');
670 | return jsonResponse();
671 | });
672 |
673 | const RequestWithHeaders = FetchRequest.extend({
674 | host: 'http://example.com',
675 | headers: {
676 | 'Content-Type': 'application/json',
677 | 'Other-key': 'Other Value',
678 | },
679 | });
680 |
681 | const service = RequestWithHeaders.create();
682 | return service.request('http://example.com', {
683 | headers: {
684 | 'Per-Request-Key': 'Some value',
685 | },
686 | });
687 | });
688 |
689 | test('can get the full list from class and request options', function (assert) {
690 | const RequestWithHeaders = FetchRequest.extend({
691 | headers: {
692 | 'Content-Type': 'application/vnd.api+json',
693 | 'Other-Value': 'Some Value',
694 | },
695 | });
696 |
697 | const service = RequestWithHeaders.create();
698 | const headers = { 'Third-Value': 'Other Thing' };
699 | assert.strictEqual(Object.keys(service._getFullHeadersHash()).length, 2);
700 | assert.strictEqual(
701 | Object.keys(service._getFullHeadersHash(headers)).length,
702 | 3,
703 | );
704 | assert.strictEqual(Object.keys(service.headers).length, 2);
705 | });
706 | });
707 |
708 | test('it creates a detailed error message for unmatched server errors with an AJAX payload', function (assert) {
709 | const response = [
710 | 408,
711 | { 'Content-Type': 'application/json' },
712 | JSON.stringify({ errors: ['Some error response'] }),
713 | ];
714 | this.server.get('/posts', () => response);
715 |
716 | const service = FetchRequest.create();
717 | return service
718 | .request('/posts')
719 | .then(function () {
720 | throw new Error('success handler should not be called');
721 | })
722 | .catch(function (result) {
723 | assert.ok(result.message.includes('Some error response'));
724 | assert.ok(result.message.includes('GET'));
725 | assert.ok(result.message.includes('/posts'));
726 | assert.strictEqual(result.status, 408);
727 | });
728 | });
729 |
730 | test('it creates a detailed error message for unmatched server errors with a text payload', function (assert) {
731 | const response = [
732 | 408,
733 | { 'Content-Type': 'text/html' },
734 | 'Some error response',
735 | ];
736 | this.server.get('/posts', () => response);
737 |
738 | const service = FetchRequest.create();
739 | return service
740 | .request('/posts')
741 | .then(function () {
742 | throw new Error('success handler should not be called');
743 | })
744 | .catch(function (result) {
745 | assert.ok(result.message.includes('Some error response'));
746 | assert.ok(result.message.includes('GET'));
747 | assert.ok(result.message.includes('/posts'));
748 | assert.strictEqual(result.status, 408);
749 | });
750 | });
751 |
752 | test('it throws an error when the user tries to use `.get` to make a request', function (assert) {
753 | const service = FetchRequest.create();
754 | service.set('someProperty', 'foo');
755 |
756 | assert.strictEqual(service.get('someProperty'), 'foo');
757 |
758 | assert.throws(function () {
759 | service.get('/users');
760 | });
761 |
762 | assert.throws(function () {
763 | service.get('/users', {});
764 | });
765 | });
766 |
767 | test('it JSON encodes JSON request data automatically per contentType', function (assert) {
768 | this.server.post('/test', ({ requestBody }) => {
769 | const { foo } = JSON.parse(requestBody);
770 | assert.strictEqual(foo, 'bar');
771 | return jsonResponse();
772 | });
773 |
774 | const RequestWithHeaders = FetchRequest.extend({
775 | contentType: 'application/json',
776 | });
777 |
778 | const service = RequestWithHeaders.create();
779 | return service.post('/test', {
780 | data: {
781 | foo: 'bar',
782 | },
783 | });
784 | });
785 |
786 | test('it JSON encodes JSON:API request data automatically per contentType', function (assert) {
787 | this.server.post('/test', ({ requestBody }) => {
788 | const { foo } = JSON.parse(requestBody);
789 | assert.strictEqual(foo, 'bar');
790 | return jsonResponse();
791 | });
792 |
793 | const RequestWithHeaders = FetchRequest.extend({
794 | contentType: 'application/vnd.api+json',
795 | });
796 |
797 | const service = RequestWithHeaders.create();
798 | return service.post('/test', {
799 | data: {
800 | foo: 'bar',
801 | },
802 | });
803 | });
804 |
805 | test('it JSON encodes JSON request data automatically per Content-Type header', function (assert) {
806 | this.server.post('/test', ({ requestBody }) => {
807 | const { foo } = JSON.parse(requestBody);
808 | assert.strictEqual(foo, 'bar');
809 | return jsonResponse();
810 | });
811 |
812 | const RequestWithHeaders = FetchRequest.extend({
813 | headers: {
814 | 'Content-Type': 'application/json',
815 | },
816 | });
817 |
818 | const service = RequestWithHeaders.create();
819 | return service.post('/test', {
820 | data: {
821 | foo: 'bar',
822 | },
823 | });
824 | });
825 |
826 | test('it JSON encodes JSON:API request data automatically per Content-Type header', function (assert) {
827 | this.server.post('/test', ({ requestBody }) => {
828 | const { foo } = JSON.parse(requestBody);
829 | assert.strictEqual(foo, 'bar');
830 | return jsonResponse();
831 | });
832 |
833 | const RequestWithHeaders = FetchRequest.extend({
834 | headers: {
835 | 'Content-Type': 'application/vnd.api+json',
836 | },
837 | });
838 |
839 | const service = RequestWithHeaders.create();
840 | return service.post('/test', {
841 | data: {
842 | foo: 'bar',
843 | },
844 | });
845 | });
846 |
847 | test('it does not JSON encode query parameters when JSON:API headers are present', function (assert) {
848 | this.server.get('/test', ({ queryParams }) => {
849 | const { foo } = queryParams;
850 | assert.strictEqual(foo, 'bar');
851 | return jsonResponse();
852 | });
853 |
854 | const RequestWithHeaders = FetchRequest.extend({
855 | headers: {
856 | 'Content-Type': 'application/vnd.api+json',
857 | },
858 | });
859 |
860 | const service = RequestWithHeaders.create();
861 | return service.request('/test', {
862 | data: {
863 | foo: 'bar',
864 | },
865 | });
866 | });
867 |
868 | test('it JSON encodes JSON:API "extension" request data automatically', function (assert) {
869 | this.server.post('/test', ({ requestBody }) => {
870 | const { foo } = JSON.parse(requestBody);
871 | assert.strictEqual(foo, 'bar');
872 | return jsonResponse();
873 | });
874 |
875 | const RequestWithHeaders = FetchRequest.extend({
876 | headers: {
877 | 'Content-Type': 'application/vnd.api+json; ext="ext1,ext2"',
878 | },
879 | });
880 |
881 | const service = RequestWithHeaders.create();
882 | return service.post('/test', {
883 | data: {
884 | foo: 'bar',
885 | },
886 | });
887 | });
888 |
889 | module('URL building', function () {
890 | class NamespaceLeadingSlash extends FetchRequest {
891 | static get slashType() {
892 | return 'leading slash';
893 | }
894 |
895 | get namespace() {
896 | return '/bar';
897 | }
898 | }
899 |
900 | class NamespaceTrailingSlash extends FetchRequest {
901 | static get slashType() {
902 | return 'trailing slash';
903 | }
904 |
905 | get namespace() {
906 | return 'bar/';
907 | }
908 | }
909 |
910 | class NamespaceTwoSlash extends FetchRequest {
911 | static get slashType() {
912 | return 'leading and trailing slash';
913 | }
914 |
915 | get namespace() {
916 | return '/bar/';
917 | }
918 | }
919 |
920 | class NamespaceNoSlash extends FetchRequest {
921 | static get slashType() {
922 | return 'no slashes';
923 | }
924 |
925 | get namespace() {
926 | return 'bar';
927 | }
928 | }
929 |
930 | const hosts = [
931 | { hostType: 'trailing slash', host: 'http://foo.com/' },
932 | { hostType: 'no trailing slash', host: 'http://foo.com' },
933 | ];
934 |
935 | [
936 | NamespaceLeadingSlash,
937 | NamespaceTrailingSlash,
938 | NamespaceTwoSlash,
939 | NamespaceNoSlash,
940 | ].forEach((Klass) => {
941 | const req = Klass.create();
942 |
943 | hosts.forEach((exampleHost) => {
944 | const { host } = exampleHost;
945 |
946 | test(`correctly handles ${Klass.slashType} when the host has ${exampleHost.hostType}`, function (assert) {
947 | ['/baz', 'baz'].forEach((segment) => {
948 | assert.strictEqual(
949 | req._buildURL(segment, { host }),
950 | 'http://foo.com/bar/baz',
951 | );
952 | });
953 | ['/baz/', 'baz/'].forEach((segment) => {
954 | assert.strictEqual(
955 | req._buildURL(segment, { host }),
956 | 'http://foo.com/bar/baz/',
957 | );
958 | });
959 | });
960 | });
961 | });
962 |
963 | test('correctly handles a host provided on the request options', function (assert) {
964 | const req = FetchRequest.create();
965 | assert.strictEqual(
966 | req._buildURL('/baz', { host: 'http://foo.com' }),
967 | 'http://foo.com/baz',
968 | );
969 | });
970 |
971 | test('correctly handles no namespace or host', function (assert) {
972 | const req = FetchRequest.create();
973 | assert.strictEqual(req._buildURL('/baz'), '/baz');
974 | assert.strictEqual(req._buildURL('baz'), 'baz');
975 | });
976 |
977 | test('does not build the URL if the namespace is already present', function (assert) {
978 | class RequestWithNamespace extends FetchRequest {
979 | get namespace() {
980 | return 'api';
981 | }
982 | }
983 |
984 | const req = RequestWithNamespace.create();
985 | assert.strictEqual(
986 | req._buildURL('/api/post'),
987 | '/api/post',
988 | 'URL provided with leading slash',
989 | );
990 | assert.strictEqual(
991 | req._buildURL('api/post'),
992 | 'api/post',
993 | 'URL provided without leading slash',
994 | );
995 | });
996 |
997 | test('correctly handles a URL with leading part similar to the namespace', function (assert) {
998 | class RequestWithNamespace extends FetchRequest {
999 | get namespace() {
1000 | return 'admin';
1001 | }
1002 | }
1003 |
1004 | const req = RequestWithNamespace.create();
1005 | assert.strictEqual(
1006 | req._buildURL('/admin_users/post'),
1007 | 'admin/admin_users/post',
1008 | );
1009 | });
1010 |
1011 | module('building relative URLs', function () {
1012 | test('works with a relative namespace with no trailing slash', function (assert) {
1013 | class RelativeNamespace extends FetchRequest {
1014 | get namespace() {
1015 | return 'api/v1';
1016 | }
1017 | }
1018 |
1019 | const req = RelativeNamespace.create();
1020 | assert.strictEqual(req._buildURL('foobar'), 'api/v1/foobar');
1021 | });
1022 |
1023 | test('works with a relative namespace with a trailing slash', function (assert) {
1024 | class RelativeNamespace extends FetchRequest {
1025 | get namespace() {
1026 | return 'api/v1/';
1027 | }
1028 | }
1029 |
1030 | const req = RelativeNamespace.create();
1031 | assert.strictEqual(req._buildURL('foobar'), 'api/v1/foobar');
1032 | });
1033 | });
1034 |
1035 | module('building a URL with a host', function () {
1036 | test('correctly handles a host without a namespace', function (assert) {
1037 | class HostWithoutNamespace extends FetchRequest {
1038 | get host() {
1039 | return 'http://foo.com';
1040 | }
1041 | }
1042 |
1043 | const req = HostWithoutNamespace.create();
1044 | assert.strictEqual(req._buildURL('baz'), 'http://foo.com/baz');
1045 | });
1046 |
1047 | test('does not build the URL if the host is already present', function (assert) {
1048 | class RequestWithHost extends FetchRequest {
1049 | get host() {
1050 | return 'https://foo.com';
1051 | }
1052 | }
1053 |
1054 | const req = RequestWithHost.create();
1055 | assert.strictEqual(
1056 | req._buildURL('https://foo.com/posts'),
1057 | 'https://foo.com/posts',
1058 | );
1059 | });
1060 | });
1061 | });
1062 |
1063 | module('error handlers', function () {
1064 | // test('handles a TimeoutError correctly', function(assert) {
1065 | // this.server.get('/posts', jsonFactory(200), 2);
1066 | // const service = FetchRequest.create();
1067 | // return service
1068 | // .request('/posts', { timeout: 1 })
1069 | // .then(function() {
1070 | // throw new Error('success handler should not be called');
1071 | // })
1072 | // .catch(function(reason) {
1073 | // assert.ok(isTimeoutError(reason));
1074 | // assert.strictEqual(reason.payload, null);
1075 | // assert.strictEqual(reason.status, -1);
1076 | // });
1077 | // });
1078 |
1079 | function errorHandlerTest(status, errorClass) {
1080 | test(`handles a ${status} response correctly and preserves the payload`, function (assert) {
1081 | this.server.get(
1082 | '/posts',
1083 | jsonFactory(status, {
1084 | errors: [{ id: 1, message: 'error description' }],
1085 | }),
1086 | );
1087 | const service = FetchRequest.create();
1088 | return service
1089 | .request('/posts')
1090 | .then(function () {
1091 | throw new Error('success handler should not be called');
1092 | })
1093 | .catch(function (reason) {
1094 | assert.ok(reason instanceof errorClass);
1095 | assert.notStrictEqual(reason.payload, undefined);
1096 | assert.strictEqual(reason.status, status);
1097 |
1098 | const { errors } = reason.payload;
1099 |
1100 | assert.ok(errors && typeOf(errors) === 'array');
1101 | assert.strictEqual(errors[0].id, 1);
1102 | assert.strictEqual(errors[0].message, 'error description');
1103 | });
1104 | });
1105 | }
1106 |
1107 | errorHandlerTest(401, UnauthorizedError);
1108 | errorHandlerTest(403, ForbiddenError);
1109 | errorHandlerTest(409, ConflictError);
1110 | errorHandlerTest(410, GoneError);
1111 | errorHandlerTest(422, InvalidError);
1112 | errorHandlerTest(400, BadRequestError);
1113 | errorHandlerTest(500, ServerError);
1114 | errorHandlerTest(502, ServerError);
1115 | errorHandlerTest(510, ServerError);
1116 |
1117 | test(`handles response with malformed json`, function (assert) {
1118 | this.server.get('/posts', () => [
1119 | 200,
1120 | { 'Content-Type': 'application/json' },
1121 | 'foobar',
1122 | ]);
1123 | const service = FetchRequest.create();
1124 | return service
1125 | .request('/posts')
1126 | .then(function () {
1127 | throw new Error('success handler should not be called');
1128 | })
1129 | .catch(function (reason) {
1130 | assert.notStrictEqual(reason.payload, undefined);
1131 | assert.ok(reason.message.includes('Unexpected token'));
1132 | assert.ok(reason.message.includes('GET'));
1133 | assert.ok(reason.message.includes('/posts'));
1134 | assert.strictEqual(reason.status, 200);
1135 | });
1136 | });
1137 | });
1138 | });
1139 |
--------------------------------------------------------------------------------