Leitura e gravação de URL mais seguras em JavaScript moderno
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)
Há 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 hosturl.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
- Escrito originalmente por Steve Sewell em Safer URL reading and writing in modern JavaScript.