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

oieduardorabelo

Eduardo Rabelo

Posted on January 25, 2023

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)
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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

url = url + '&foo=bar'
Enter fullscreen mode Exit fullscreen mode

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)
  }`
Enter fullscreen mode Exit fullscreen mode

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)
  }`
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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...
});
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

searchParams.get(name)

Obtenha o valor de um determinado parâmetro:

url.searchParams.get('page') // '1'
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

searchParams.set(name, value)

Defina o valor de um parâmetro:

url.searchParams.set('page', '1')
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

searchParams.delete(name)

Remova totalmente um parâmetro da URL:

url.searchParams.delete('page')
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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

new URL('/blog', 'https://builder.io')
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
oieduardorabelo
Eduardo Rabelo

Posted on January 25, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related