<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Nicole Aragão</title>
    <description>The latest articles on Forem by Nicole Aragão (@nicole_aragao).</description>
    <link>https://forem.com/nicole_aragao</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3877519%2Ff4638ebe-a31e-46b6-9377-51a5e532701b.jpg</url>
      <title>Forem: Nicole Aragão</title>
      <link>https://forem.com/nicole_aragao</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/nicole_aragao"/>
    <language>en</language>
    <item>
      <title>Da Autoria à Exegese: O Código como Ato de Linguagem na Era da IA</title>
      <dc:creator>Nicole Aragão</dc:creator>
      <pubDate>Mon, 20 Apr 2026 00:45:26 +0000</pubDate>
      <link>https://forem.com/nicole_aragao/da-autoria-a-exegese-o-codigo-como-ato-de-linguagem-na-era-da-ia-g37</link>
      <guid>https://forem.com/nicole_aragao/da-autoria-a-exegese-o-codigo-como-ato-de-linguagem-na-era-da-ia-g37</guid>
      <description>&lt;p&gt;Com o uso diário de IA no trabalho, venho percebendo uma mudança que no começo parecia só desconforto: leio muito mais código do que escrevo. E quanto mais isso se repete, mais questiono o que exatamente mudou - não na ferramenta, mas no meu papel. A sensação de que, se não estou digitando, não estou produzindo, é persistente, e sei que muitos engenheiros têm sentido o mesmo. Recentemente, numa conversa com meu colega Rafael Jeffman, percebi que o desconforto tinha nome e raiz.&lt;/p&gt;

&lt;p&gt;Antes da engenharia, estudei Letras - sempre fui a garota de humanas, cercada de livros. Meu escritório até hoje tem mais estante do que monitor. Linguística era a minha matéria favorita, e fazia anos que esse conhecimento parecia guardado. Até que a IA o tornou relevante de novo.&lt;/p&gt;

&lt;p&gt;Olhando com essa lente - depois de meses achando que a profissão ia desaparecer - o que vejo é quase o oposto. A IA tornou visível uma dimensão da profissão que sempre subestimamos: a engenharia de software tem uma natureza profundamente linguística. Agora que a máquina assume o rascunho, nos cabe o trabalho que sempre foi o mais difícil: garantir que o código diz o que precisa dizer.&lt;/p&gt;

&lt;p&gt;Na linguística e na teoria literária, esse trabalho tem nome: exegese - a interpretação crítica de um texto, buscando significado que não está na superfície. É exatamente o que fazemos ao ler código gerado por IA.&lt;/p&gt;

&lt;h2&gt;
  
  
  O Código como Ato Performativo
&lt;/h2&gt;

&lt;p&gt;Para entender por que revisar código de IA é tão diferente de revisar um documento qualquer, vale recorrer a uma distinção da filosofia da linguagem.&lt;/p&gt;

&lt;p&gt;J.L. Austin, em &lt;em&gt;Quando Dizer é Fazer&lt;/em&gt; (&lt;em&gt;How to Do Things with Words&lt;/em&gt;), separou dois tipos de expressão. Uma &lt;strong&gt;constativa&lt;/strong&gt; descreve a realidade: "a porta está aberta". Uma &lt;strong&gt;performativa&lt;/strong&gt; altera a realidade: "declaro a sessão aberta". O performativo não é verdadeiro ou falso - ele é bem-sucedido ou fracassado. E o seu sucesso depende de contexto, autoridade e intenção.&lt;/p&gt;

&lt;p&gt;Código é performativo. Um &lt;code&gt;UPDATE&lt;/code&gt; no banco não descreve uma mudança - ele &lt;em&gt;causa&lt;/em&gt; a mudança. Um &lt;code&gt;deploy&lt;/code&gt; não relata que o sistema mudou - ele &lt;em&gt;muda&lt;/em&gt; o sistema. Cada linha de código que chega à produção é um ato que altera o estado do mundo.&lt;/p&gt;

&lt;p&gt;Austin distinguiu duas camadas em todo ato de fala: o &lt;strong&gt;locutório&lt;/strong&gt; - a frase em si, gramaticalmente correta - e o &lt;strong&gt;ilocucionário&lt;/strong&gt; - a intenção por trás dela, o que o falante pretende causar.&lt;/p&gt;

&lt;p&gt;É aqui que a IA se torna interessante. Um LLM produz atos locutórios com competência impressionante: código sintaticamente correto, idiomático, que compila e frequentemente roda. Mas um LLM não tem intenção. Ele não &lt;em&gt;pretende&lt;/em&gt; nada ao gerar um &lt;code&gt;UPDATE&lt;/code&gt; - ele calcula a sequência mais provável de tokens. O ato ilocucionário está ausente.&lt;/p&gt;

&lt;p&gt;Quem atribui a intenção somos nós. Quando revisamos o código gerado por IA e decidimos que aquele &lt;code&gt;UPDATE&lt;/code&gt; deve ir para produção, somos nós que assumimos a força ilocucionária do ato: "este comando expressa a minha intenção, e eu me responsabilizo pelo que ele vai causar no sistema".&lt;/p&gt;

&lt;h2&gt;
  
  
  Frames: Quando a Sintaxe Está Certa e o Significado Está Errado
&lt;/h2&gt;

&lt;p&gt;Se Austin explica &lt;em&gt;o que o código faz&lt;/em&gt; (performar), falta entender &lt;em&gt;como o significado funciona&lt;/em&gt; dentro do código. E já que estamos nessa de desenterrar a faculdade de Letras, trago mais um: a Semântica de Frames de Charles Fillmore.&lt;/p&gt;

&lt;p&gt;Fillmore argumentou que nós não entendemos palavras isoladas - entendemos palavras &lt;em&gt;dentro de cenários&lt;/em&gt;. A palavra "comprar" ativa automaticamente um frame completo: comprador, vendedor, mercadoria, preço, transação. Você não precisa mencionar todos esses elementos; eles estão implícitos no frame. Quem ouve "ela comprou" já pressupõe que havia um vendedor, um preço e algo sendo vendido.&lt;/p&gt;

&lt;p&gt;No código, os mesmos princípios operam. Um &lt;code&gt;Payment&lt;/code&gt; não é apenas uma classe com atributos. No seu sistema, esse nome carrega um frame inteiro: o que acontece quando dois pagamentos chegam ao mesmo tempo? A mesma requisição pode cobrar duas vezes? Quanto tempo esperar pelo gateway bancário antes de desistir? Como estornar? Quem audita? Nenhum desses elementos precisa estar no nome da classe - mas todo engenheiro experiente que lê &lt;code&gt;Payment&lt;/code&gt; no seu contexto os ativa instantaneamente.&lt;/p&gt;

&lt;p&gt;A IA não faz isso. Ela opera sobre padrões estatísticos do texto de treinamento. Quando gera código para um &lt;code&gt;Payment&lt;/code&gt;, ela tende a ativar o frame mais &lt;em&gt;comum&lt;/em&gt; - o que aparece na maioria dos tutoriais e repositórios públicos. Esse frame é, tipicamente, o mais simples: um objeto com campos, validações básicas, um CRUD. Ele está sintaticamente correto e semanticamente incompleto para o &lt;em&gt;seu&lt;/em&gt; sistema.&lt;/p&gt;

&lt;p&gt;O resultado é código que passa no compilador mas falha no contexto. A classe &lt;code&gt;Payment&lt;/code&gt; existe, os tipos estão certos, a API funciona - mas o frame é o de um tutorial, não o de um sistema financeiro real. A concorrência não foi considerada. A idempotência não está lá. O engenheiro que aceita esse código sem checar o frame está importando as premissas de um contexto genérico para um domínio específico.&lt;/p&gt;

&lt;p&gt;O trabalho do engenheiro, nesse cenário, é curadoria semântica: verificar se o frame que a IA ativou corresponde ao frame do sistema real. Isso exige conhecimento de domínio que nenhum LLM possui - porque o frame do &lt;em&gt;seu&lt;/em&gt; sistema não está nos dados de treinamento. Está na cabeça do time, nas decisões de arquitetura que nunca viraram documentação, na experiência acumulada de quem já viu aquele &lt;code&gt;Payment&lt;/code&gt; quebrar em produção. Linguagens de programação são lógicas, mas existem para representar um mundo que não é: domínios de negócio são subjetivos, contextuais, cheios de exceções que nenhum tutorial prevê. Esse conhecimento se constrói vivendo o sistema, não treinando sobre código público.&lt;/p&gt;

&lt;h2&gt;
  
  
  O Que Isso Muda na Prática
&lt;/h2&gt;

&lt;p&gt;Se código é ato performativo e significado depende de frames, o papel do engenheiro na era da IA se redefine em termos concretos:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ler código de IA é mais exigente do que escrever o próprio.&lt;/strong&gt; Quando você escreve, externaliza um modelo mental que já possui - a intenção e o frame já estão na sua cabeça. Quando lê código gerado por IA, você precisa reconstruir um modelo mental que nunca existiu. Não há intenção para rastrear, não há autor para consultar.&lt;/p&gt;

&lt;p&gt;Você constrói o significado sozinha, e depois valida se esse significado é compatível com o seu sistema. Isso é cognitivamente mais caro - não por incompetência, mas pela natureza do processo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tenho a impressão de que sênioridade, hoje, se mede mais pela capacidade de ler do que de escrever.&lt;/strong&gt; A máquina é muito boa em gerar código sintaticamente correto. O que ela não faz é duvidar do próprio output. Um engenheiro júnior e um sênior recebem o mesmo código gerado por IA - a diferença é o que cada um pergunta antes de aceitar. O júnior pergunta "funciona?". O sênior pergunta "funciona &lt;em&gt;no meu contexto&lt;/em&gt;?". Essa segunda pergunta é inteiramente linguística: é uma verificação de frame.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;O valor está na precisão semântica, não na velocidade de produção.&lt;/strong&gt; A pergunta que define um engenheiro deixou de ser "quão rápido você entrega código?" e passou a ser "quão preciso é o seu entendimento do que esse código faz no sistema?". Escrever rápido virou commodity. Ler com precisão é habilidade rara e difícil de desenvolver. &lt;/p&gt;

&lt;p&gt;Não acredito que estamos perdendo relevância. Acredito que estamos sendo forçados a exercer a parte do trabalho que sempre foi a mais difícil e a mais valiosa - e que, por ser invisível, nunca recebeu o reconhecimento que merecia. A IA não nos substituiu. Ela tornou impossível fingir que digitar era a parte que importava.&lt;/p&gt;

&lt;p&gt;E se você é como eu, que ainda sente prazer genuíno em escrever código do zero - esse prazer não precisa desaparecer. Mas o que fazemos com ele, e como ele se encaixa numa rotina cada vez mais exegética, é uma conversa para outro texto.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>discuss</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How Do You Debug What You Can't Reproduce? An IPv6 case study in minimal test environments</title>
      <dc:creator>Nicole Aragão</dc:creator>
      <pubDate>Tue, 14 Apr 2026 00:11:46 +0000</pubDate>
      <link>https://forem.com/nicole_aragao/how-do-you-debug-what-you-cant-reproduce-an-ipv6-case-study-in-minimal-test-environments-f50</link>
      <guid>https://forem.com/nicole_aragao/how-do-you-debug-what-you-cant-reproduce-an-ipv6-case-study-in-minimal-test-environments-f50</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;A customer reported that their Discovery scans were failing in a pure IPv6 environment. The error messages pointed to two different issues:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Scenario A&lt;/em&gt;: LocationParseError: Failed to parse - when using raw IPv6 addresses&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Scenario B&lt;/em&gt;: OSError: [Errno 101] Network is unreachable - when using FQDNs&lt;/p&gt;

&lt;p&gt;The tricky part? The customer was running a single-stack IPv6 OpenShift cluster - a rare configuration typically seen in telecommunications environments on bare-metal infrastructure. This wasn't a cluster we could just spin up on the fly. Setting one up would require specialized OpenShift expertise and dedicated resources, neither of which were readily available to us.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Challenge: You Can't Always Replicate the Customer's Environment&lt;/strong&gt;&lt;br&gt;
My first instinct was to provision an identical environment. I started researching options:&lt;/p&gt;

&lt;p&gt;Convert an existing cluster to IPv6? &lt;br&gt;
IPv6 on AWS? Complex, not available in standard templates.&lt;br&gt;
Request cluster time from internal infrastructure? Unclear if possible, and unknown timeline.&lt;br&gt;
I was stuck. How do you debug a network connectivity issue if you can't reproduce the network?&lt;/p&gt;

&lt;h2&gt;
  
  
  A Brief Detour: Debugging as a Discipline
&lt;/h2&gt;

&lt;p&gt;Before diving into the solution, it's worth stepping back to consider what we're actually doing when we debug. Debugging isn't just trial and error - it's a systematic process with well-established principles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Scientific Method in Debugging&lt;/strong&gt;&lt;br&gt;
Debugging, at its core, follows the scientific method:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Observe: Gather evidence (error messages, logs, customer reports)&lt;/li&gt;
&lt;li&gt;Hypothesize: Form a theory about what's causing the issue&lt;/li&gt;
&lt;li&gt;Predict: Determine what you'd expect to see if your hypothesis is correct&lt;/li&gt;
&lt;li&gt;Test: Create an experiment to validate or invalidate the hypothesis&lt;/li&gt;
&lt;li&gt;Iterate: Refine your hypothesis based on results&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In our case, we had two hypotheses for Scenario B:&lt;/p&gt;

&lt;p&gt;Hypothesis 1: The Quadlet-managed container network lacks IPv6 routes&lt;/p&gt;

&lt;p&gt;Hypothesis 2: Python's socket library behaves differently than curl under certain conditions&lt;/p&gt;

&lt;p&gt;Both needed testing - but we couldn't test them without a reproducible environment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Minimal Reproducible Example (MRE)&lt;/strong&gt;&lt;br&gt;
The concept of a Minimal Reproducible Example (MRE) is fundamental in open source and debugging communities. An MRE is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Minimal: Contains only what's necessary to reproduce the issue&lt;/li&gt;
&lt;li&gt;Reproducible: Can be reliably repeated&lt;/li&gt;
&lt;li&gt;Example: Demonstrates the specific problem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key insight is that you don't need to replicate the customer's entire environment. You need to replicate the conditions that cause the bug. Everything else is noise. For our IPv6 issue, the customer's OpenShift cluster had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Control plane nodes&lt;/li&gt;
&lt;li&gt;Worker nodes&lt;/li&gt;
&lt;li&gt;Operators and workloads&lt;/li&gt;
&lt;li&gt;Storage configurations&lt;/li&gt;
&lt;li&gt;Network policies&lt;/li&gt;
&lt;li&gt;...and much more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But none of that mattered for our bug. We needed exactly two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An IPv6-only network&lt;/li&gt;
&lt;li&gt;An HTTP endpoint to connect to&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fault Isolation: Divide and Conquer&lt;/strong&gt;&lt;br&gt;
Fault isolation is the practice of systematically narrowing down where a problem occurs by testing components independently. Instead of asking "why doesn't this work?", you ask "at which layer does it stop working?"The process:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify the layers/components in your system&lt;/li&gt;
&lt;li&gt;Test each layer in isolation&lt;/li&gt;
&lt;li&gt;Find the boundary where behavior changes&lt;/li&gt;
&lt;li&gt;Focus investigation there&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For container networking, the layers are:&lt;/p&gt;

&lt;p&gt;Host System → Container Runtime → Container Network → Application&lt;/p&gt;

&lt;p&gt;If something works at the host level but fails in a container, you've isolated the issue to container networking. This dramatically reduces the search space.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Insight: What Are We Actually Testing?
&lt;/h2&gt;

&lt;p&gt;During a team discussion, my colleagues offered a critical insight: "You don't need OpenShift. You just need to prove that the scanner can connect to an IPv6 address. If the log says 'API returned 404,' that means connectivity worked - the scanner reached the server."This is MRE thinking in action. We stripped away everything except the core question: can a container reach an IPv6 endpoint?I didn't need a full OpenShift cluster with operators, nodes, and workloads. I needed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A server listening on an IPv6 address&lt;/li&gt;
&lt;li&gt;The ability to make HTTP requests to it&lt;/li&gt;
&lt;li&gt;The same container environment the customer was using&lt;/li&gt;
&lt;li&gt;A simple Python HTTP server would do.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Setup: Building a Minimal Reproduction Environment
&lt;/h2&gt;

&lt;p&gt;I created the simplest possible IPv6-only environment on my Fedora laptop using libvirt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Create an IPv6-Only Network&lt;/strong&gt;&lt;br&gt;
`&lt;/p&gt;

&lt;p&gt;ipv6-only&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;nat ipv6='yes'&amp;gt;

  &amp;lt;port start='1024' end='65535'/&amp;gt;

&amp;lt;/nat&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;







&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;dhcp&amp;gt;

  &amp;lt;range start='fd00:dead:beef::100' end='fd00:dead:beef::1ff'/&amp;gt;

&amp;lt;/dhcp&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;`&lt;/p&gt;

&lt;p&gt;I used a ULA (Unique Local Address) range - fd00:dead:beef::/64 - which is perfect for local testing. No need for public IPv6 allocation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Create a RHEL 9 VM&lt;/strong&gt;&lt;br&gt;
I installed a minimal RHEL 9 VM on this network, disabling IPv4 entirely during installation. The VM received the address fd00:dead:beef::126 via DHCPv6.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Start a Mock Server&lt;/strong&gt;&lt;br&gt;
Instead of deploying OpenShift, I started a one-liner HTTP server:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;python3 -m http.server --bind fd00:dead:beef::126 6443&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;That's it. A functioning "API endpoint" in 30 seconds. Total infrastructure cost: One VM, zero cloud resources, about 20 minutes of setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Validation: Applying Fault Isolation
&lt;/h2&gt;

&lt;p&gt;With the environment ready, I applied fault isolation to test connectivity layer by layer. This systematic approach would prove crucial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: Host to VM&lt;/strong&gt;&lt;br&gt;
Hypothesis: Basic IPv6 connectivity works from my laptop.&lt;/p&gt;

&lt;p&gt;`$ curl -v http://[fd00:dead:beef::126]:6443/&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connected to fd00:dead:beef::126 port 6443&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&amp;lt; HTTP/1.0 200 OK`&lt;/p&gt;

&lt;p&gt;Result: Confirmed. Moving to the next layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: Container with Host Networking&lt;/strong&gt;&lt;br&gt;
Hypothesis: Containers can reach IPv6 when sharing the host's network stack.&lt;/p&gt;

&lt;p&gt;`$ podman run --rm --network=host curlimages/curl \&lt;/p&gt;

&lt;p&gt;curl -s -o /dev/null -w '%{http_code}' http://[fd00:dead:beef::126]:6443/&lt;/p&gt;

&lt;p&gt;200`&lt;/p&gt;

&lt;p&gt;Result: Confirmed. The issue isn't with containers in general.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: Container with Default Bridge&lt;/strong&gt;&lt;br&gt;
Hypothesis: Podman's default bridge network supports IPv6.&lt;/p&gt;

&lt;p&gt;`$ podman run --rm curlimages/curl \&lt;/p&gt;

&lt;p&gt;curl -s -o /dev/null -w '%{http_code}' http://[fd00:dead:beef::126]:6443/&lt;/p&gt;

&lt;p&gt;200`&lt;/p&gt;

&lt;p&gt;Result: Confirmed. The default bridge works on my system.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 4: Application Container via Quadlet&lt;/strong&gt;&lt;br&gt;
Hypothesis: The Quadlet-managed environment behaves the same as manual container runs.&lt;/p&gt;

&lt;p&gt;`$ podman exec quipucords-server python3 -c "&lt;/p&gt;

&lt;p&gt;import socket&lt;/p&gt;

&lt;p&gt;socket.create_connection(('fd00:dead:beef::126', 6443), timeout=5)&lt;/p&gt;

&lt;p&gt;"&lt;/p&gt;

&lt;p&gt;FAILED: [Errno 101] Network is unreachable`&lt;/p&gt;

&lt;p&gt;Result: Failed. We found the boundary. The fault was isolated: something about the Quadlet-managed network is different from the default Podman bridge or host networking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Discovery: Two Distinct Root Causes
&lt;/h2&gt;

&lt;p&gt;Fault isolation pointed me to the Quadlet network. But as I investigated further, I discovered that Scenario A (URL parsing) was a completely separate issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario A: The URL Was Malformed&lt;/strong&gt;&lt;br&gt;
Testing the URL parsing directly:&lt;/p&gt;

&lt;p&gt;`$ podman run --rm --network=host python:3.12 python3 -c "&lt;/p&gt;

&lt;p&gt;import requests&lt;/p&gt;

&lt;p&gt;requests.get('&lt;a href="http://fd00:dead:beef::126:6443/'" rel="noopener noreferrer"&gt;http://fd00:dead:beef::126:6443/'&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;"`&lt;/p&gt;

&lt;p&gt;Failed to parse: &lt;a href="http://fd00:dead:beef::126:6443/" rel="noopener noreferrer"&gt;http://fd00:dead:beef::126:6443/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problem is visible in the URL itself. IPv6 addresses contain colons, and so does the port separator. The parser sees fd00:dead:beef::126:6443 and cannot determine where the host ends and the port begins.Per RFC 2732, IPv6 addresses in URLs must be wrapped in square brackets:&lt;/p&gt;

&lt;p&gt;Correct:   http://[fd00:dead:beef::126]:6443/&lt;/p&gt;

&lt;p&gt;Incorrect: &lt;a href="http://fd00:dead:beef::126:6443/" rel="noopener noreferrer"&gt;http://fd00:dead:beef::126:6443/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our code was building URLs by simple string concatenation:&lt;/p&gt;

&lt;p&gt;host_uri = f"{protocol}://{host}:{port}"&lt;/p&gt;

&lt;p&gt;This works for IPv4 and hostnames, but fails for IPv6.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scenario B: The Container Network Had No IPv6&lt;/strong&gt;&lt;br&gt;
The Quadlet-managed containers use a custom Podman network. I inspected it:&lt;/p&gt;

&lt;p&gt;`$ podman network inspect systemd-quipucords&lt;/p&gt;

&lt;p&gt;{&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"ipv6_enabled": false,

"subnets": [

    {"subnet": "10.89.0.0/24", "gateway": "10.89.0.1"}

]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;}`&lt;/p&gt;

&lt;p&gt;There it was: ipv6_enabled: false. The network only had an IPv4 subnet. The containers literally had no path to reach IPv6 addresses.The root cause was our installer's network configuration file:&lt;/p&gt;

&lt;p&gt;[Network]&lt;/p&gt;

&lt;p&gt;An empty configuration. Podman defaults to IPv4-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: One Line Each
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Fix for Scenario A (Application Code)&lt;/strong&gt;&lt;br&gt;
Create a utility function to wrap IPv6 addresses:&lt;/p&gt;

&lt;p&gt;`import ipaddress&lt;/p&gt;

&lt;p&gt;def format_host_for_url(host: str) -&amp;gt; str:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"""Wrap IPv6 addresses in brackets for URL construction."""

&lt;p&gt;try:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ipaddress.IPv6Address(host)

return f"[{host}]"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;except ipaddress.AddressValueError:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;return host`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h1&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Usage&lt;br&gt;
&lt;/h1&gt;

&lt;p&gt;host_uri = f"{protocol}://{format_host_for_url(host)}:{port}"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix for Scenario B (Installer Configuration)&lt;/strong&gt;&lt;br&gt;
Enable IPv6 in the Quadlet network definition:&lt;/p&gt;

&lt;p&gt;`[Network]&lt;/p&gt;

&lt;p&gt;IPv6=true&lt;br&gt;
`&lt;br&gt;
After applying this change and restarting the containers:&lt;/p&gt;

&lt;p&gt;`$ podman exec quipucords-server python3 -c "&lt;/p&gt;

&lt;p&gt;import socket&lt;/p&gt;

&lt;p&gt;socket.create_connection(('fd00:dead:beef::126', 6443), timeout=5)&lt;/p&gt;

&lt;p&gt;print('SUCCESS')&lt;/p&gt;

&lt;p&gt;"&lt;br&gt;
`&lt;br&gt;
SUCCESS: Can reach IPv6 VM&lt;/p&gt;

&lt;p&gt;The hypothesis was validated. The fix worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Minimal Reproduction Beats Exact Replication&lt;/strong&gt;&lt;br&gt;
I didn't need a complex bare-metal IPv6 OpenShift cluster. I needed a VM with a Python one-liner. The key was applying MRE thinking: what is the minimum environment that reproduces the specific failure?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Test Each Layer Separately&lt;/strong&gt;&lt;br&gt;
Fault isolation works. By testing host → container → Quadlet progressively, I discovered that the issue only appeared in the Quadlet-managed environment. Without this layered approach, I might have spent hours debugging the wrong component.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Hypothesize, Then Test&lt;/strong&gt;&lt;br&gt;
I started with two hypotheses about Scenario B. The scientific method gave me a framework: form a hypothesis, predict what you'd see if it's true, test it, iterate. This prevents the aimless debugging that wastes hours.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Question Your Assumptions&lt;/strong&gt;&lt;br&gt;
I assumed "curl works in the container" meant Python would work too. It didn't - different network namespaces, different behavior. The customer's report that "curl works but the app fails" was a valid and important clue that I initially dismissed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Two Bugs Can Look Like One&lt;/strong&gt;&lt;br&gt;
The customer reported "scans fail in IPv6." What they actually had was two unrelated bugs that both happened to affect IPv6:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A URL formatting bug (would fail even with perfect networking)&lt;/li&gt;
&lt;li&gt;A network configuration bug (would fail even with perfect URLs)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treating them as one problem would have led to incomplete fixes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Collaboration Accelerates Everything&lt;/strong&gt;&lt;br&gt;
My teammates suggested the minimal reproduction approach. Without that conversation, I might still be waiting for infrastructure access. Sometimes the best debugging tool is a five-minute Standup discussion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Customer environments are often impossible to replicate exactly. But exact replication is rarely necessary. The principles of debugging — minimal reproducible examples, fault isolation, and the scientific method - provide a framework for making progress without perfect information.By identifying the core behavior you need to test and building the simplest possible environment that exhibits it, you can debug effectively without access to production systems.In this case, a RHEL VM, a Python HTTP server, and a Quadlet configuration file were enough to find and fix two bugs that would have been nearly impossible to debug in the customer's actual environment.The total time from "I can't reproduce this" to "both fixes validated"? About four hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;p&gt;"Why Programs Fail" by Andreas Zeller — A systematic approach to debugging&lt;br&gt;
RFC 2732 - Format for Literal IPv6 Addresses in URLs&lt;br&gt;
Julia Evans' debugging zines - Accessible, visual explanations of debugging techniques&lt;br&gt;
Podman Quadlet documentation - Understanding systemd-managed containers&lt;/p&gt;

</description>
      <category>debugging</category>
      <category>devops</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
