HTTP Request
What This Example Shows
This example demonstrates how to handle I/O operations, such as HTTP requests, within a Bubble Tea application without blocking the user interface. It shows a simple GET request to check a server's status and display the result asynchronously.
The key here is using Bubble Tea's command architecture (tea.Cmd) to perform long-running tasks like HTTP requests outside the main event loop, keeping the UI responsive.
Understanding I/O in Terminal User Interfaces
In Terminal User Interfaces (TUIs), responsiveness is crucial since they run in a single-threaded event loop. Performing blocking I/O operations, like HTTP requests, directly in the update loop would freeze the app, making it unresponsive to user input.
Bubble Tea solves this with asynchronous commands: functions that run in separate goroutines and return messages with results, allowing the app to update state without blocking.
This approach is perfect for creating responsive apps that handle network operations gracefully, ensuring users can interact (e.g., quit) even during long tasks.
Here's what happens:
- The app starts and initiates an HTTP request via a command
- The UI shows a "Checking..." message while the request runs in the background
- When complete, a message updates the model with the status or error, and the view reflects it
How It Works
Define the Model
The model holds the request state:
type model struct {
status int
err error
}It tracks the HTTP status code or any error.
Create Custom Messages
Define messages for success and error:
type statusMsg int
type errMsg struct{ error }These will be returned by the command.
Perform the HTTP Request
The command handles the GET request:
func checkServer() tea.Msg {
c := &http.Client{Timeout: 10 * time.Second}
res, err := c.Get(url)
if err != nil {
return errMsg{err}
}
defer res.Body.Close()
return statusMsg(res.StatusCode)
}It runs asynchronously and returns a message.
HTTP requests depend on network conditions; always handle timeouts and errors to avoid indefinite hangs.
Code Breakdown
The app uses a simple model to store the response state:
type model struct {
status int
err error
}
const url = "https://charm.sh/"status defaults to 0, and err is nil initially.
Custom messages for command results:
type statusMsg int
type errMsg struct{ error }
func (e errMsg) Error() string { return e.error.Error() }statusMsg carries the status code; errMsg wraps errors.
The command performs the request:
func checkServer() tea.Msg {
c := &http.Client{
Timeout: 10 * time.Second,
}
res, err := c.Get(url)
if err != nil {
return errMsg{err}
}
defer res.Body.Close()
return statusMsg(res.StatusCode)
}It uses a timed-out client for safety.
Start the program and init the command:
func main() {
p := tea.NewProgram(model{})
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
func (m model) Init() tea.Cmd {
return checkServer
}Init triggers the request immediately.
Handle messages from the command:
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case statusMsg:
m.status = int(msg)
return m, tea.Quit
case errMsg:
m.err = msg
return m, nil
default:
return m, nil
}
}Updates state and quits on success.
Allow quitting during the request:
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
return m, tea.Quit
default:
return m, nil
}This keeps the app responsive.
The View
The view renders based on the current state:
func (m model) View() string {
s := fmt.Sprintf("Checking %s...", url)
if m.err != nil {
s += fmt.Sprintf("something went wrong: %s", m.err)
} else if m.status != 0 {
s += fmt.Sprintf("%d %s", m.status, http.StatusText(m.status))
}
return s + "\n"
}It shows progress, errors, or results.
Final Code
package main
import (
"fmt"
"log"
"net/http"
"time"
tea "github.com/charmbracelet/bubbletea"
)
const url = "https://charm.sh/"
type model struct {
status int
err error
}
type statusMsg int
type errMsg struct{ error }
func (e errMsg) Error() string { return e.error.Error() }
func main() {
p := tea.NewProgram(model{})
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
}
func (m model) Init() tea.Cmd {
return checkServer
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c", "esc":
return m, tea.Quit
default:
return m, nil
}
case statusMsg:
m.status = int(msg)
return m, tea.Quit
case errMsg:
m.err = msg
return m, nil
default:
return m, nil
}
}
func (m model) View() string {
s := fmt.Sprintf("Checking %s...", url)
if m.err != nil {
s += fmt.Sprintf("something went wrong: %s", m.err)
} else if m.status != 0 {
s += fmt.Sprintf("%d %s", m.status, http.StatusText(m.status))
}
return s + "\n"
}
func checkServer() tea.Msg {
c := &http.Client{
Timeout: 10 * time.Second,
}
res, err := c.Get(url)
if err != nil {
return errMsg{err}
}
defer res.Body.Close() // nolint:errcheck
return statusMsg(res.StatusCode)
}How is this guide?