Client net/http

Permintaan GET dan POST serta batas waktu client.

Daftar isi

Paket net/http menyediakan sisi client, yaitu bagian program yang mengirim permintaan (request) ke server dan menerima respons (response). Topik ini membahas fungsi http.Get untuk permintaan GET, http.Post untuk permintaan POST, pembacaan tubuh respons, serta penetapan batas waktu melalui http.Client. Seluruh contoh menggunakan httptest.NewServer, yaitu fungsi pustaka standar yang menyalakan server latihan pada alamat lokal, sehingga program dapat dieksekusi dan diuji tanpa menghubungi internet. Setelah membacanya, permintaan HTTP dapat dikirim dengan pola yang aman, mulai dari membaca tubuh respons sampai menutupnya dengan benar.

Permintaan GET dengan http.Get#

Fungsi http.Get menerima satu URL dan mengembalikan dua nilai, yaitu *http.Response dan error. Respons memuat StatusCode, tubuh respons pada Body bertipe io.ReadCloser, dan tajuk-tajuknya. Tubuh respons wajib ditutup dengan Close setelah selesai dibaca, karena koneksi yang menyertainya tidak dapat dipakai ulang sebelum tubuh respons terbaca habis. Pembacaannya dilakukan dengan io.ReadAll sebagaimana aliran data pada umumnya. Error dari http.Get hanya muncul ketika permintaan gagal terkirim, misalnya server tidak dapat dijangkau, sedangkan respons berstatus 404 tetap dikembalikan sebagai respons yang sah. Program berikut menyalakan server latihan, mengirim permintaan GET, lalu membaca hasilnya.

go permintaan-get.go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
)

func main() {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "paket %s siap diambil", r.URL.Query().Get("id"))
	}))
	defer srv.Close()

	resp, err := http.Get(srv.URL + "/paket?id=gudang-3")
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	defer resp.Body.Close()

	fmt.Println("status:", resp.StatusCode)
	isi, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(string(isi))
}
text Keluaran
status: 200
paket gudang-3 siap diambil

Server latihan pada program itu menanggapi parameter kueri id, sehingga permintaan /paket?id=gudang-3 mengembalikan teks yang mengulang nilai parameternya. Fungsi http.Get sendiri tidak mengetahui bahwa tujuannya server latihan, karena yang dilihatnya hanya URL.

Tajuk respons dapat dibaca melalui resp.Header, misalnya resp.Header.Get("Content-Type") untuk mengetahui bentuk isinya. Apabila permintaan membutuhkan tajuk khusus atau metode lain, http.NewRequest membentuk permintaan terlebih dahulu, lalu permintaan itu dikirim melalui metode Do milik http.Client. Kedua bentuk tersebut mengembalikan tipe respons yang sama, sehingga pembacaan tubuh dan penutupannya tetap mengikuti pola pada program di atas. Pemakaian pada target sungguhan berbentuk sama, hanya URL-nya yang ditukar dengan alamat internet seperti berikut.

go Permintaan GET ke server internet
// tidak-dijalankan
resp, err := http.Get("https://example.com/berita?id=terbaru")
if err != nil {
	log.Println("error:", err)
	return
}
defer resp.Body.Close()

Permintaan POST dengan http.Post#

Fungsi http.Post mengirim data pada tubuh permintaan, sehingga cocok untuk pengiriman formulir dan dokumen JSON. Argumennya terdiri atas URL, tipe isi (content type), dan sumber data bertipe io.Reader, yang lazim berupa strings.NewReader atau bytes.NewBuffer. Untuk formulir, tipe isinya application/x-www-form-urlencoded dan datanya ditulis dalam bentuk kunci=nilai. Sisi server membaca data formulir tersebut dengan r.FormValue. Program berikut mengirim satu kolom formulir ke server latihan yang langsung membalasnya.

go permintaan-post.go
package main

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
)

func main() {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "form diterima: "+r.FormValue("paket"))
	}))
	defer srv.Close()

	resp, err := http.Post(srv.URL+"/kirim", "application/x-www-form-urlencoded", strings.NewReader("paket=gudang-3"))
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	defer resp.Body.Close()

	isi, err := io.ReadAll(resp.Body)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println("status:", resp.StatusCode)
	fmt.Println(string(isi))
}
text Keluaran
status: 200
form diterima: gudang-3

Baris strings.NewReader("paket=gudang-3") menyiapkan tubuh permintaan tanpa membuat berkas atau buffer tambahan, dan http.Post mengalirkannya ke server. Data formulir yang diterima server dibaca dengan FormValue, sehingga server membalas dengan isi kolom paket. Untuk dokumen JSON, polanya sama dengan tipe isi application/json dan tubuh berupa hasil json.Marshal. Panjang tubuh permintaan tidak perlu dihitung sendiri, karena http.Post membaca pembacanya sampai habis dan menetapkan ukurannya secara otomatis untuk sumber berupa string maupun buffer. Permintaan tanpa data tetap memerlukan tipe isinya, dan http.NoBody dapat dikirim apabila tubuhnya memang kosong.

Batas waktu pada client#

Fungsi http.Get memakai http.DefaultClient yang tidak memiliki batas waktu, sehingga permintaan pada server yang tidak menjawab dapat menunggu tanpa batas. Untuk pemakaian sungguhan, buat http.Client sendiri dengan field Timeout berisi time.Duration. Apabila respons tidak selesai sebelum batas waktu, permintaan dibatalkan dan errornya dikembalikan, tanpa perlu mengatur goroutine pembatal secara manual. Program berikut menyalakan server yang sengaja lambat, kemudian mengirim permintaan dengan batas waktu yang lebih pendek daripada keterlambatannya.

go batas-waktu.go
package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"time"
)

func main() {
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(300 * time.Millisecond)
		fmt.Fprint(w, "respons terlambat")
	}))
	defer srv.Close()

	klien := &http.Client{Timeout: 100 * time.Millisecond}
	_, err := klien.Get(srv.URL + "/lambat")
	if err != nil {
		fmt.Println("permintaan dibatalkan karena melewati batas waktu")
		return
	}
	fmt.Println("permintaan selesai")
}
text Keluaran
permintaan dibatalkan karena melewati batas waktu

Handler server menunggu 300 milidetik sebelum membalas, sedangkan client hanya bersedia menunggu 100 milidetik, sehingga permintaan selalu dibatalkan dan pesan pembatalannya muncul. Nilai batas waktu yang wajar bergantung pada layanan yang dihubungi, dan satu nilai Timeout pada client mencakup sambungan, pengiriman, serta pembacaan tubuh respons sekaligus.

Nilai Timeout tersebut mencakup seluruh tahapan permintaan, mulai dari sambungan TCP hingga byte terakhir tubuh respons. Untuk kendali yang lebih halus, http.Client juga menerima Transport yang memuat batas waktu sambungan dan batas waktu koneksi yang menganggur (idle). Pada tingkat permintaan, context.Context yang dibatalkan dapat dilekatkan melalui NewRequestWithContext, sehingga satu permintaan tertentu dapat dihentikan lebih awal tanpa memengaruhi client lainnya.

Ketiga program dapat dijalankan dari terminal sebagaimana berikut.

bash
# siapkan folder kerja untuk topik ini
mkdir -p /tmp/gopustaka/client-http
cp permintaan-get.go permintaan-post.go batas-waktu.go /tmp/gopustaka/client-http/
cd /tmp/gopustaka/client-http

# jalankan program permintaan GET
go run permintaan-get.go

# jalankan program permintaan POST
go run permintaan-post.go

# jalankan program batas waktu
go run batas-waktu.go
text Keluaran
status: 200
paket gudang-3 siap diambil
status: 200
form diterima: gudang-3
permintaan dibatalkan karena melewati batas waktu

Kesalahan umum#

  • Tidak menutup resp.Body. Koneksi yang menyertai tubuh respons tetap terbuka, sehingga banyak permintaan beruntun dapat menghabiskan sambungan yang tersedia. Perbaikannya, tutup tubuh respons dengan defer resp.Body.Close() tepat setelah error permintaannya diperiksa.
  • Memakai http.Get untuk program sungguhan tanpa batas waktu. http.DefaultClient menunggu tanpa batas, sehingga satu server yang macet dapat menahan program selamanya. Perbaikannya, buat http.Client sendiri dengan field Timeout.
  • Mengabaikan StatusCode. Respons 404 atau 500 tetap dikembalikan tanpa error, sehingga program yang langsung membaca tubuhnya memproses pesan kesalahan sebagai data. Perbaikannya, periksa resp.StatusCode sebelum isi respons dipakai.
  • Menutup tubuh respons sebelum selesai dibaca. Pembacaan yang tersisa terpotong dan sambungan yang menyertainya tidak dapat dipakai ulang. Perbaikannya, baca seluruh tubuhnya terlebih dahulu, misalnya dengan io.ReadAll, sebelum Close dipanggil.
  • Membaca resp.Body dua kali. Tubuh respons adalah aliran sekali baca, sehingga pembacaan kedua menghasilkan data kosong. Perbaikannya, simpan hasil pembacaan pertama pada variabel bila isinya diperlukan lebih dari satu kali.

Ringkasan#

  • http.Get dan http.Post mengembalikan *http.Response beserta error, dengan tubuh respons yang wajib ditutup setelah dibaca.
  • httptest.NewServer menyalakan server latihan pada alamat lokal, sehingga client dapat diuji tanpa menghubungi internet.
  • Data POST dikirim melalui io.Reader beserta tipe isinya, dan sisi server membacanya dengan FormValue.
  • http.Client dengan field Timeout membatalkan permintaan yang melewati batas waktu, menggantikan http.DefaultClient yang menunggu tanpa batas.

Referensi#