ExodusMovement / fetch

MIT License
0 stars 0 forks source link

feat: implement safe url concatenation #4

Closed ChALkeR closed 1 year ago

ChALkeR commented 1 year ago

Ok, at long last, I think we have a solution for this that we could use without major rewrite.

It contains several methods: { url, urlComponent, urlBase, urlUnwrap }.

urlComponent is a template literal that escapes everything except already escaped components. It returns a special class that is convertible to string.

urlUnwrap is a function that validates and unwraps instances of that class.

url is a template literal, that returns an URL instance with some additional checks. Otherwise it escapes all components except already escaped by urlComponent. As a special case, it allows the very first component to be an URL instance (aka base) and doesn't escape it then.

Simple usecase:

const arg = 'val/ue'
console.log(url`https://example.org/buz/${arg}`) // new URL(https://example.org/buz/val%2Fue')
const base = new URL('https://example.com/foo')
const arg = 'val/ue'
console.log(url`${base}/bar/${arg}`) // new URL('https://example.com/foo/bar/val%2Fue')

Note: it throws on '..' or when the result is an invalid URL.


E.g. this code:

const BASE_URL = 'https://example.com'
...
request(path) {
  return fetchival(this.baseUrl, { })(path)
}
await this.request(`foo/bar/${encodeURIComponent(arg)}`).get()

Is (mostly, omitting extra safeguards) equivalent to:

const BASE_URL = 'https://example.com'
...
request(path) {
  return fetchival(this.baseUrl, { })(path)
}
await this.request(urlComponent`foo/bar/${arg}`).get()

Or

const BASE_URL = 'https://example.com'
...
request(path) {
  const baseUrl = new URL(this.baseUrl)
  return fetchival(url`${baseUrl}/${path}`, { })
}
await this.request(urlComponent`foo/bar/${arg}`).get()

Or

const BASE_URL = 'https://example.com'
...
request(path) {
  const baseUrl = new URL(this.baseUrl)
  return fetchival(url`${baseUrl}${path}`, { })
}
await this.request(urlComponent`/foo/bar/${arg}`).get()

Or

const BASE_URL = 'https://example.com'
...
get base() {
  return new URL(this.baseUrl)
}
request(path) {
  return fetchival(path, { })
}
await this.request(url`${this.base}/foo/bar/${arg}`).get()

Or same with fetchival replaced by anything else as the last code sample doesn't rely on fetchival url concatenation anymore.


Some extra checks are implemented, e.g. this code will throw:

const base = new URL('https://example.com/foo')
console.log(url`${base}sd/fds`) 

While adding the missing / anywhere would work.

ChALkeR commented 1 year ago

Published 1.3.0-beta.1 for testing