Skip to content

Commit

Permalink
fix: Handle 503 Blocked response when calling rootCozyUrl
Browse files Browse the repository at this point in the history
When calling `rootCozyUrl` we want the method to throw a specific
exception if the Cozy is in a "blocked" state

If a `503 Blocked` is returned we conclude that there is a Cozy behind
the URL but we cannot check if the URL points to the Cozy's root or to
a specific slug. So it is safer to throw an exception that should then
be handled by the caller

BREAKING CHANGE: rootCozyUrl() can now throw a BlockedCozyError
exception if the target Cozy is blocked
  • Loading branch information
Ldoppea committed Jul 21, 2023
1 parent c863101 commit 603cb08
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 11 deletions.
3 changes: 2 additions & 1 deletion docs/api/cozy-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ cozy-client
## Classes

* [Association](classes/Association.md)
* [BlockedCozyError](classes/BlockedCozyError.md)
* [BulkEditError](classes/BulkEditError.md)
* [CozyClient](classes/CozyClient.md)
* [CozyLink](classes/CozyLink.md)
Expand Down Expand Up @@ -615,7 +616,7 @@ The root Cozy URL

*Defined in*

[packages/cozy-client/src/helpers/urlHelper.js:249](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/helpers/urlHelper.js#L249)
[packages/cozy-client/src/helpers/urlHelper.js:269](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/helpers/urlHelper.js#L269)

***

Expand Down
39 changes: 39 additions & 0 deletions docs/api/cozy-client/classes/BlockedCozyError.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[cozy-client](../README.md) / BlockedCozyError

# Class: BlockedCozyError

## Hierarchy

* `Error`

**`BlockedCozyError`**

## Constructors

### constructor

**new BlockedCozyError**(`url`)

*Parameters*

| Name | Type |
| :------ | :------ |
| `url` | `any` |

*Overrides*

Error.constructor

*Defined in*

[packages/cozy-client/src/helpers/urlHelper.js:138](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/helpers/urlHelper.js#L138)

## Properties

### url

**url**: `any`

*Defined in*

[packages/cozy-client/src/helpers/urlHelper.js:141](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/helpers/urlHelper.js#L141)
4 changes: 2 additions & 2 deletions docs/api/cozy-client/classes/InvalidCozyUrlError.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Error.constructor

*Defined in*

[packages/cozy-client/src/helpers/urlHelper.js:138](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/helpers/urlHelper.js#L138)
[packages/cozy-client/src/helpers/urlHelper.js:146](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/helpers/urlHelper.js#L146)

## Properties

Expand All @@ -36,4 +36,4 @@ Error.constructor

*Defined in*

[packages/cozy-client/src/helpers/urlHelper.js:141](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/helpers/urlHelper.js#L141)
[packages/cozy-client/src/helpers/urlHelper.js:149](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/helpers/urlHelper.js#L149)
3 changes: 2 additions & 1 deletion packages/cozy-client/src/helpers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ export {
rootCozyUrl,
InvalidRedirectLinkError,
InvalidCozyUrlError,
InvalidProtocolError
InvalidProtocolError,
BlockedCozyError
} from './urlHelper'

export { dehydrate } from './dehydrateHelper'
41 changes: 40 additions & 1 deletion packages/cozy-client/src/helpers/urlHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,14 @@ export class InvalidProtocolError extends Error {
}
}

export class BlockedCozyError extends Error {
constructor(url) {
super(`Blocked cozy ${url.toString()}`)

this.url = url
}
}

export class InvalidCozyUrlError extends Error {
constructor(url) {
super(`URL ${url.toString()} does not seem to be a valid Cozy URL`)
Expand Down Expand Up @@ -187,6 +195,10 @@ const wellKnownUrl = url => uri(url) + '/.well-known/change-password'
* - a 401 response status means the pointed page requires authentication so the
* origin is probably pointing to a cozy-app. In that case we should consider this
* URL to be invalid
* - a 503 response status with a "Blocked" reason means the pointed page is a Cozy
* but it is blocked. In that case we consider that the url is a valid Cozy origin
* but we want the method to throw as we cannot verify if the URL points to the
* Cozy's root or to a specifc slug. The caller is responsible to handle that exception
* - another status means there aren't any Cozy behind to the given origin
*
* @param {object} url Object of URL elements
Expand All @@ -196,13 +208,20 @@ const wellKnownUrl = url => uri(url) + '/.well-known/change-password'
*
* @returns {Promise<boolean>} True if we believe there's a Cozy behind the given origin
* @throws {InvalidCozyUrlError} Thrown when we know for sure there aren't any Cozy behind the given origin
* @throws {BlockedCozyError} Thrown when we know for sure there is Cozy behind the given origin but it is in a "Blocked" state
*/
const isValidOrigin = async url => {
const { status } = await fetch(wellKnownUrl(url))
const response = await fetch(wellKnownUrl(url))
const { status } = response

if (status === 404) {
throw new InvalidCozyUrlError(url)
}

if (await isResponseAboutBlockedCozy(response)) {
throw new BlockedCozyError(url)
}

return status === 200
}

Expand Down Expand Up @@ -286,3 +305,23 @@ export const rootCozyUrl = async url => {
// without success. So bail out and let the user provide a valid one.
throw new InvalidCozyUrlError(url)
}

/**
* Check if the given response is about a Cozy being blocked
*
* @param {Response} response - Fetch API response
* @returns {Promise<boolean>} true if the response is about a Cozy being blocked, false otherwize
*/
const isResponseAboutBlockedCozy = async response => {
if (response.status !== 503) return false

const contentType = response.headers.get('content-type')
const isJson = contentType && contentType.indexOf('json') >= 0
const data = await (isJson ? response.json() : response.text())

if (data?.some?.(reason => reason.title === 'Blocked')) {
return true
}

return false
}
59 changes: 58 additions & 1 deletion packages/cozy-client/src/helpers/urlHelper.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import {
rootCozyUrl,
InvalidRedirectLinkError,
InvalidCozyUrlError,
InvalidProtocolError
InvalidProtocolError,
BlockedCozyError
} from './urlHelper'

describe('generateWebLink', () => {
Expand Down Expand Up @@ -420,4 +421,60 @@ describe('rootCozyUrl', () => {
rootCozyUrl(new URL('https://missing.mycozy.cloud'))
).rejects.toBeInstanceOf(InvalidCozyUrlError)
})

it('should reject if Blocked cozy', async () => {
fetch.mockResponse(() => {
return Promise.resolve({
headers: {
'content-type': 'application/json; charset=UTF-8'
},
body: JSON.stringify([
{
status: '503',
title: 'Blocked',
code: 'UNKNOWN',
detail: 'The Cozy is blocked for an unknown reason',
source: {}
}
]),
ok: false,
status: 503,
statusText: '',
type: 'default',
url: 'https://camillenimbus.com/.well-known/change-password'
})
})

await expect(
rootCozyUrl(new URL('https://camillenimbus.com'))
).rejects.toBeInstanceOf(BlockedCozyError)
})

it('should reject on 503 error (if not Blocked)', async () => {
fetch.mockResponse(() => {
return Promise.resolve({
headers: {
'content-type': 'application/json; charset=UTF-8'
},
body: JSON.stringify([
{
status: '503',
title: 'Some error',
code: 'UNKNOWN',
detail: 'Some error description',
source: {}
}
]),
ok: false,
status: 503,
statusText: '',
type: 'default',
url: 'https://camillenimbus.com/.well-known/change-password'
})
})

await expect(
rootCozyUrl(new URL('https://camillenimbus.com'))
).rejects.toBeInstanceOf(InvalidCozyUrlError)
})
})
3 changes: 2 additions & 1 deletion packages/cozy-client/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export {
rootCozyUrl,
InvalidRedirectLinkError,
InvalidCozyUrlError,
InvalidProtocolError
InvalidProtocolError,
BlockedCozyError
} from './helpers'
export { cancelable, isQueryLoading, hasQueryBeenLoaded } from './utils'
export { getQueryFromState } from './store'
Expand Down
3 changes: 2 additions & 1 deletion packages/cozy-client/src/index.node.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ export {
ensureFirstSlash,
rootCozyUrl,
InvalidCozyUrlError,
InvalidProtocolError
InvalidProtocolError,
BlockedCozyError
} from './helpers'
export { cancelable } from './utils'
export { getQueryFromState } from './store'
Expand Down
2 changes: 1 addition & 1 deletion packages/cozy-client/types/helpers/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { dehydrate } from "./dehydrateHelper";
export { deconstructCozyWebLinkWithSlug, deconstructRedirectLink, generateWebLink, ensureFirstSlash, rootCozyUrl, InvalidRedirectLinkError, InvalidCozyUrlError, InvalidProtocolError } from "./urlHelper";
export { deconstructCozyWebLinkWithSlug, deconstructRedirectLink, generateWebLink, ensureFirstSlash, rootCozyUrl, InvalidRedirectLinkError, InvalidCozyUrlError, InvalidProtocolError, BlockedCozyError } from "./urlHelper";
4 changes: 4 additions & 0 deletions packages/cozy-client/types/helpers/urlHelper.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ export class InvalidProtocolError extends Error {
constructor(url: any);
url: any;
}
export class BlockedCozyError extends Error {
constructor(url: any);
url: any;
}
export class InvalidCozyUrlError extends Error {
constructor(url: any);
url: any;
Expand Down
2 changes: 1 addition & 1 deletion packages/cozy-client/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ export { manifest, models };
export { QueryDefinition, Q, Mutations, MutationTypes, getDoctypeFromOperation } from "./queries/dsl";
export { Association, HasMany, HasOne, HasOneInPlace, HasManyInPlace, HasManyTriggers } from "./associations";
export { isReferencedBy, isReferencedById, getReferencedBy, getReferencedById } from "./associations/helpers";
export { deconstructCozyWebLinkWithSlug, deconstructRedirectLink, dehydrate, generateWebLink, ensureFirstSlash, rootCozyUrl, InvalidRedirectLinkError, InvalidCozyUrlError, InvalidProtocolError } from "./helpers";
export { deconstructCozyWebLinkWithSlug, deconstructRedirectLink, dehydrate, generateWebLink, ensureFirstSlash, rootCozyUrl, InvalidRedirectLinkError, InvalidCozyUrlError, InvalidProtocolError, BlockedCozyError } from "./helpers";
export { cancelable, isQueryLoading, hasQueryBeenLoaded } from "./utils";
export { queryConnect, queryConnectFlat, withClient } from "./hoc";
2 changes: 1 addition & 1 deletion packages/cozy-client/types/index.node.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ import * as models from "./models";
export { manifest, models };
export { QueryDefinition, Mutations, MutationTypes, getDoctypeFromOperation, Q } from "./queries/dsl";
export { Association, HasMany, HasOne, HasOneInPlace, HasManyInPlace, HasManyTriggers } from "./associations";
export { deconstructCozyWebLinkWithSlug, deconstructRedirectLink, dehydrate, generateWebLink, ensureFirstSlash, rootCozyUrl, InvalidCozyUrlError, InvalidProtocolError } from "./helpers";
export { deconstructCozyWebLinkWithSlug, deconstructRedirectLink, dehydrate, generateWebLink, ensureFirstSlash, rootCozyUrl, InvalidCozyUrlError, InvalidProtocolError, BlockedCozyError } from "./helpers";

0 comments on commit 603cb08

Please sign in to comment.