Endpoints: o caminho por onde todas as requisições passam

Moroni Sauner
stonetech
Published in
6 min readMay 13, 2021
Imagem com fundo de céu azul, parcialmente nublado, dando destaque a um poste direcional com 4 placas apontando para diferentes direções.

Fala, galera! Quem mora em condomínio sabe que toda e qualquer encomenda que chegue para os condôminos tem que passar pela portaria. Mas o que isso tem a ver com programação? Essa é uma simples analogia sobre o tema deste artigo: endpoints Phoenix.

Os endpoints são o lugar por onde todas as requisições passam em todas as aplicações web, ou seja, funcionam como a portaria do seu prédio: tudo passa por ela antes de chegar na sua casa.

Aqui pretendo falar do ponto de vista técnico sobre o que acontece com o Phoenix, do momento em que recebe uma requisição até a resposta para ela.

Plug-se

É muito difícil falar sobre Phoenix sem abordar o Plug. Quem já trabalha com o Phoenix sabe que eles são bastante ligados, afinal, todo o framework foi construído em cima do Plug. Inclusive, posso garantir a vocês, não é como um filme de terror, mas: “We see Plug… all the time”.

Um menino com aparentemente amendrontado com o texto: “Vejo Plugs em todos os lugares”. Uma alusão a uma cena do filme Sexto sentido.

Phoenix é um framework que basicamente dá ferramentas muito boas para você conseguir trabalhar com o Plug puro, que é um ‘pouquinho’ mais complicado.

Assim, os módulos Endpoint, Router e Controllers são uma mão na roda para fazer o que você precisa fazer. E isso só muda quando você começa a mexer com channels e sockets. Mas não posso dar mais detalhes sobre isso porque ainda não fiz essa experiência.

A estrutura básica do Plug

Para mostrar como funciona uma aplicação web Phoenix, vou mostrar a estrutura básica do Plug. A primeira coisa que você precisa saber é que trata-se de um módulo simples, com duas funções.

Não precisava colocar esse behaviour aqui. Só preciso desse init/1 e desse call/2. E o que esse call/2 vai fazer? Basicamente só vai mexer com a estrutura que representa uma conexão, que é esse o primeiro argumento conn, ou seja, o %Plug.Conn{}. Aí ele fará a manipulação alterar essa struct e jogar para o próximo plug para que ele faça a parte dele.

Nesse caso aqui, como ele é bem simples, tudo que ele faz é trocar o verbo HTTP de HEAD para GET, alterando o campo method. Se der match, ele atualiza essa struct e só repassa para o próximo Plug.

A struct é basicamente essa do código abaixo. Ela representa a conexão e a ganhamos a partir do momento que o cliente manda alguma requisição para o nosso servidor.

Tem alguns campos importantes aqui. É interessante reparar que alguns deles estão marcados como unfetched. Nesse momento, você não tem um valor definido para eles, por essa razão que o valor deles é essa struct %Plug.Conn.Unfetched{}. Porém, o Endpoint do qual falo lá no título é este daqui:

É o caminho que toda requisição faz e que fará com que ele passe por alguns Plugs. No final, ele cairá para o nosso Router, que já terá rotas definidas.

Ele vai fazer o match da rota que está no path_info com essas strings depois das funções get e post. Através dessas strings que o router verá o que faz sentido e fará o parse dos path_params.

Vale falar também dessas funções. Existe uma função para cada verbo HTTP. Assim, mesmo que a rota “/” se repita, o Router não vai se confundir, pois além da rota em si ele olha para o verbo HTTP e sabe diferenciar qual match é correto para o caso.

Super-gêmeos, parsear!

As pessoas mais novas não devem lembrar, mas tempos atrás Hanna-Barbera, mago dos quadrinhos, criou os irmãos alienígenas que ativam e desativam o poder de se transformar em qualquer coisa. Bom, parsear é mais ou menos isso: transformar A em B.

Um homem e uma mulher, gêmeos, vestindo uniforme de herói, um em cada ponta da imagem. Ambos estão com os braços esticados, tocando somente o punho. O texto “Super-gêmeos, parsear!” está escrito na parte inferior da imagem.

Não à toa, existe um plug, o Plug.Parsers, que é justamente o cara responsável por ler a requisição e traduzir o campo body_params para um map. Além disso, ele também lê o campo query_string e coloca o mapa correspondente em outro campo, que é o query_params. Cada vez que algum plug faz o parse dos campos body_params, query_params e path_params ele vai dar um Map.merge/2 no campo params da struct %Plug.Conn{}.

Ou seja, aqui o params agrupa todos esses três tipos de parâmetros — path, query e body — e todos eles ficam disponíveis no campo params.

Aí se você precisar necessariamente saber se um parâmetro veio do path, query ou body você precisará conferir no campo query_params, path_params ou body_params, se for o caso.

Aqui é um código hipotético, mas ele basicamente explica o que o router faz. Não é mais do que isso: ver qual o controller deu match com a rota para que possa chamar a função que eu defini no Controller.

Agora vem a mágica: pegamos a struct conn e o params e aplicamos a lógica do nosso negócio em cima dessa requisição.

O Phoenix oferece outros recursos que são realmente muito bons de conhecer, mas os três dos quais falei são mais fundamentais se você quer entender bem e não se perder no Phoenix.

Os deveres do endpoint

O sua “portaria”, o endpoint, tem alguns deveres a cumprir. Vamos falar deles agora. Entre suas tarefas está colocar toda essa manipulação dentro de uma árvore de supervisão. Dessa forma, qualquer falha faz com que a árvore de supervisão faça algo sobre isso.

Esse módulo endpoint também define um pipeline inicial para todas as requests, ou seja, todas elas vão passar por ali. Ele aproveita e guarda algumas configurações. Porém, qualquer coisa além disso a tarefa deixa de ser do endpoint e passa a ser dos plugs.

Aí você me pergunta: o que é comum encontrar no endpoint? Bom, tem alguns plugs para lidar com CORS e CSRF, que são relativos a segurança. Questões de autenticação, também podem ser feitas, como validação de token. A lib — Guardian — faz isso: ela lê o seu token e verifica se ele é válido.

Também se faz coleta de métricas no endpoint. É muito comum encontrar plugs de algumas plataformas de monitoramento e de coleta de logs. Essas plataformas costumam fornecer um plug para você colocar lá no seu endpoint e já de uma forma fácil integrar a sua aplicação com essa plataforma. Exemplos disso são o DataDog e o NewRelic. Ambos têm plugs para facilitar essa integração.

Antes de finalizar, vale a pena comentar também sobre o Plug.RequestID. Trata-se de um plug muito simples, mas extremamente útil, principalmente quando você tem uma aplicação clusterizada, ou seja, tem vários nós que vão processar alguma parte da requisição.

A função dele é definir um ID para a sua request, normalmente na chave x-request-id no header da request, embora você possa trocá-la, se quiser. Ele vai jogar isso num metadado do Logger e quando fizer a requisição para algum outro nó ou algum outro serviço do seu cluster ou pod, vai lê-lo e não vai sobrescrevê-lo. Isso fará com que todos eles tenham essa mesma ID nos metadados. Assim, você consegue facilmente colocar o ID da requisição lá no seu kibana ou equivalente e conseguirá saber por onde essa requisição passou e cada um dos pods que tenha participado no processamento da request. Os logs estarão lá bonitinhos esperando por você para que saiba tudo o que aconteceu. Tem até um nome bonito: distributed traicing.

Para fechar a conta de vez, vamos falar um pouquinho do telemetry. Ele tem a função de coletar algumas métricas. A struct %Plug.Conn{} guarda uma lista de funções a serem executadas before_send à resposta. Em outras palavras: a requisição vai passar pelo plug Plug.Telemetry que vai coletar o horário da requisição e quando a resposta da request for entregue, a função do campo before_send verá a diferença de tempo e saberá quanto durou a requisição.

Todas as partes do Phoenix estão trabalhando com essa struct do plug, então entender isso é uma mão na roda quando você precisa fazer algo um pouco mais elaborado. Ajuda lembrar que, na verdade, por baixo, quem está trabalhando é o plug e não o Phoenix.

--

--