Leitura e gravação de URL mais seguras em JavaScript moderno

Eduardo Rabelo
5 min readJan 25, 2023

--

Você pode, sem saber, estar escrevendo URLs de maneira insegura

Você consegue identificar o bug neste código?

const url = `https://builder.io/api/v2/content
?model=${model}&locale=${locale}?query.text=${text}`

const res = await fetch(url)

pelo menos três!

Vamos desconstrui-los a seguir:

Número 1: Caracteres separadores incorretos

Ops! Este é certamente um erro de novato, mas tão fácil de passar batido que percebi isso em meu próprio código, mesmo após 10 anos de desenvolvimento de JS.

Um culpado comum disso, em minha experiência, é depois de editar ou mover o código. Por exemplo, você tem uma URL estruturada corretamente, depois copia uma parte de uma para outra e, em seguida, não percebe que o separador de parâmetros foi ordenado incorretamente.

Isso também pode acontecer durante a concatenação. Por exemplo:

url = url + '?foo=bar'

Mas espere, o original url pode ter um parâmetro de consulta nele. Ok, então isso deve ser:

url = url + '&foo=bar'

Mas espere, se o original url não tiver parâmetros de consulta, isso agora está errado. Argh.

Número 2: Esquecer de encodar a URL

Gah. model e locale provavelmente não precisam ser encodados, pois são valores seguros para URL, mas não parei para pensar que text pode ser todo tipo de texto, incluindo espaços em branco e caracteres especiais, o que nos causará problemas.

Então, talvez possamos corrigir até demais e jogar com mais segurança:

const url = `https://builder.io/api/v2/content
?model=${
encodeURIComponent(model)
}&locale=${
encodeURIComponent(locale)
}&query.text=${
encodeURIComponent(text)
}`

Mas as coisas estão um pouco… feias.

Número 3: Caracteres de espaço em branco acidentais

Rapaz. Para dividir essa URL longa em várias linhas, incluímos acidentalmente o caractere de nova linha e espaços extras nela, o que fará com que a busca não funcione mais como esperado.

Podemos quebrar a string corretamente agora, mas está ficando ainda mais confuso e difícil de ler:

const url = `https://builder.io/api/v2/content`
+ `?model=${
encodeURIComponent(model)
}&locale=${
encodeURIComponent(locale)
}&query.text=${
encodeURIComponent(text)
}`

Isso tudo apenas para construir uma URL correta. E vamos nos lembrar de tudo isso da próxima vez, especialmente porque o prazo está se aproximando rapidamente e precisamos lançar esse novo recurso ou consertar o mais rápido possível?

Tem que haver uma maneira melhor.

O construtor URL para o resgate

Uma solução mais limpa e segura para o desafio acima é usar o construtor de URL:

const url = new URL('https://builder.io/api/v2/content')
url.searchParams.set('model', model)
url.searchParams.set('locale', locale)
url.searchParams.set('text', text)
const res = await fetch(url.toString())

Isso resolve várias coisas para nós:

  • Os caracteres separadores estão sempre corretos ( ? para o primeiro parâmetro e depois).
  • Todos os parâmetros são codificados automaticamente.
  • Sem risco de caracteres de espaço em branco adicionais ao quebrar várias linhas para URLs longos.

Modificando URLs

Também é incrivelmente útil para situações em que estamos modificando uma URL, mas não sabemos o estado atual.

Por exemplo, em vez de ter este problema:

url += (url.includes('?') ? '&' : '?') + 'foo=bar'

Podemos fazer:

// Assumindo que `url` é uma URL
url.searchParams.set('foo', 'bar')
// Ou se URL for uma string
const structuredUrl = new URL(url)
structuredUrl.searchParams.set('foo', 'bar')
url = structuredUrl.toString()

Da mesma forma, você também pode escrever outras partes da URL:

const url = new URL('https://builder.io')
url.pathname = '/blog'      // Update the path
url.hash = '#featured' // Update the hash
url.host = 'www.builder.io' // Update the host
url.toString() // https://www.builder.io/blog#featured

Lendo valores de uma URL

Agora, o antigo problema de “eu só quero ler um parâmetro de consulta da URL atual sem uma biblioteca”, foi resolvido:

const pageParam = new URL(location.href).searchParams.get('page')

Ou, por exemplo, atualizar a URL atual com:

const url = new URL(location.href)
const currentPage = Number(url.searchParams.get('page'))
url.searchParams.set('page', String(currentPage + 1))
location.href = url.toString()

Mas isso não se limita apenas ao navegador. Também pode ser usado em Node.js

const http = require('node:http');

const server = http.createServer((req, res) => {
const url = new URL(req.url, `https://${req.headers.host}`)
// Leia o caminho, query, etc...
});

Assim como Deno:

import { serve } from "https://deno.land/std/http/mod.ts";
async function reqHandler(req: Request) {
const url = new URL(req.url)
// Leia o caminho, query, etc...
return new Response();
}
serve(reqHandler, { port: 8000 });

Propriedades para saber da URL

As instâncias de URL suportam todas as propriedades com as quais você já está acostumado no navegador, como window.location ou elementos âncora, todos os quais você pode ler e escrever:

const url = new URL('https://builder.io/blog?page=1');
url.protocol // https:
url.host // builder.io
url.pathname // /blog
url.search // ?page=1
url.href // https://builder.io/blog?page=1
url.origin // https://builder.io
url.searchParams.get('page') // 1

Ou, de relance:

Métodos URLSearchParams para conhecer

O objeto URLSearchParams, acessível em uma instância URL, url.searchParams suporta vários métodos úteis.

searchParams.has(name)

Verifique se os parâmetros de pesquisa contêm um determinado nome:

url.searchParams.has('page') // true

searchParams.get(name)

Obtenha o valor de um determinado parâmetro:

url.searchParams.get('page') // '1'

searchParams.getAll(name)

Obtenha todos os valores fornecidos para um parâmetro. Isso é útil se você permitir vários valores com o mesmo nome, como &page=1&page=2:

url.searchParams.getAll('page') // ['1']

searchParams.set(name, value)

Defina o valor de um parâmetro:

url.searchParams.set('page', '1')

searchParams.append(name, value)

Anexa um parâmetro — útil se você potencialmente oferecer suporte ao mesmo parâmetro várias vezes, como &page=1&page=2:

url.searchParams.append('page', '2')

searchParams.delete(name)

Remova totalmente um parâmetro da URL:

url.searchParams.delete('page')

Armadilhas

A única grande armadilha a saber é que todas as URLs passadas para o construtor de URL devem ser absolutas.

Por exemplo, isso lançará um erro:

new URL('/blog') // ERROR!

Você pode resolver isso, fornecendo uma origem como o segundo argumento, assim:

new URL('/blog', 'https://builder.io')

Ou, se você realmente precisa trabalhar apenas com partes da URL, como alternativa, use URLSearchParams diretamente:

const params = new URLSearchParams('page=1')
params.set('page=2')
params.toString()

URLSearchParams também tem uma outra sutileza, que é que ele também pode receber um objeto de pares de valores-chave como entrada:

const params = new URLSearchParams({
page: 1,
text: 'foobar',
})
params.set('page=2')
params.toString()

Suporte a navegador e tempo de execução

new URL é suportado em todos os navegadores modernos, bem como Node.js e Deno! ( fonte).

Créditos

--

--