HTTP Targets & Authentication
Tại Sao Quan Trọng
Cloud Tasks dispatch tasks tới HTTP endpoints bất kỳ, nhưng:
- Endpoint phải public HTTP (có external IP)
- Auth cần service account + OIDC tokens
- Headers không thể trust (không phải auth mechanism)
- Handler phải idempotent (at-least-once)
HTTP Target Endpoints
Supported Target Types
Cloud Tasks có thể dispatch tới:
✓ Cloud Run (HTTPS endpoint)
✓ App Engine (HTTPS endpoint)
✓ Compute Engine (public HTTP/HTTPS IP)
✓ GKE (public load balancer, Cloud Load Balancer frontend)
✓ On-premises (public internet)
✓ Any HTTP endpoint with external IPNot Supported
✗ Cloud Functions (use Cloud Tasks buffering)
✗ Private internal IPs (no public endpoint)
✗ Custom handlers inside VPC (without public LB)Task Creation
go
// Create task targeting Cloud Run service
client.CreateTask(ctx, &TaskRequest{
Parent: "projects/X/locations/us-central1/queues/my-queue",
Task: &Task{
HttpRequest: &HttpRequest{
Url: "https://my-service-XXXX-uc.a.run.app/api/webhook",
HttpMethod: "POST",
Headers: map[string]string{
"Content-Type": "application/json",
},
Body: []byte(`{"id": "12345"}`),
},
},
})Authentication: OIDC Tokens
Without Auth
go
// Public endpoint (no auth required)
client.CreateTask(ctx, &TaskRequest{
Parent: "...queues/public",
Task: &Task{
HttpRequest: &HttpRequest{
Url: "https://public-api.example.com/webhook",
HttpMethod: "POST",
},
},
})
// Cloud Tasks sends: POST https://public-api.example.com/webhook
// No auth headers, endpoint publicWith Auth
go
// Private endpoint (requires authentication)
client.CreateTask(ctx, &TaskRequest{
Parent: "...queues/private",
Task: &Task{
HttpRequest: &HttpRequest{
Url: "https://api.example.com/webhook",
HttpMethod: "POST",
OIDCToken: &OIDCToken{
ServiceAccountEmail: "task-executor@my-project.iam.gserviceaccount.com",
Audience: "https://api.example.com", // Receiver's HTTPS origin
},
},
},
})
// Cloud Tasks will:
// 1. Get ID token for service account
// 2. Sign token with private key
// 3. Include: Authorization: Bearer <id_token>
// 4. Send HTTP requestOIDC Token Mechanics
Flow
Step 1: Cloud Tasks request ID token
POST https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/task-executor@X.iam.gserviceaccount.com:generateIdToken
Audience: "https://api.example.com"
Step 2: Google STS returns signed JWT
{
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Step 3: Cloud Tasks includes in request
GET /webhook HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Step 4: Handler validates token
- Verify signature (public key from Google)
- Check audience matches "https://api.example.com"
- Check expiry (typically 1 hour)
- Extract claims (service account email, etc.)Token Validation (Go Example)
go
import (
"context"
"google.golang.org/api/idtoken"
)
func ValidateCloudTasksToken(ctx context.Context, bearerToken string, audience string) error {
// Remove "Bearer " prefix
token := strings.TrimPrefix(bearerToken, "Bearer ")
// Verify and validate
payload, err := idtoken.Validate(ctx, token, audience)
if err != nil {
return fmt.Errorf("invalid token: %w", err)
}
// Extract service account
serviceAccount := payload.Claims["email"].(string)
// Check if authorized
if serviceAccount != "task-executor@my-project.iam.gserviceaccount.com" {
return fmt.Errorf("unauthorized service account: %s", serviceAccount)
}
return nil
}
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
// Public endpoint, no auth required
} else {
// Private endpoint, validate token
err := ValidateCloudTasksToken(r.Context(), authHeader, "https://api.example.com")
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintf(w, `{"error": "%v"}`, err)
return
}
}
// Process task
w.WriteHeader(http.StatusOK)
}Setup: Service Account & IAM
Step 1: Create Service Account
bash
gcloud iam service-accounts create cloud-tasks-executor \
--project=my-project \
--display-name="Cloud Tasks Executor"Step 2: Grant Permissions to Cloud Tasks
bash
# Let Cloud Tasks use this service account's credentials
gcloud projects add-iam-policy-binding my-project \
--member=serviceAccount:cloud-tasks@PROJECT.iam.gserviceaccount.com \
--role=roles/iam.serviceAccountUser \
--condition=resource.name==serviceAccounts/cloud-tasks-executor@my-project.iam.gserviceaccount.comStep 3: Grant Handler Permissions
If handler is Cloud Run or App Engine:
bash
# Handler trusts the service account
gcloud run services add-iam-policy-binding my-service \
--member=serviceAccount:cloud-tasks-executor@my-project.iam.gserviceaccount.com \
--role=roles/run.invokerHeaders Injected by Cloud Tasks
Cloud Tasks automatically includes metadata headers (NOT for authentication):
http
POST /webhook HTTP/1.1
Host: api.example.com
X-CloudTasks-QueueName: projects/my-project/locations/us-central1/queues/my-queue
X-CloudTasks-TaskName: projects/my-project/locations/us-central1/queues/my-queue/tasks/task-1
X-CloudTasks-TaskRetryCount: 0
X-CloudTasks-TaskETA: 1719360000What These Are
X-CloudTasks-QueueName: Queue identifier (for reference)
X-CloudTasks-TaskName: Task ID (for idempotency tracking)
X-CloudTasks-TaskRetryCount: Retry attempt number (0 = first attempt)
X-CloudTasks-TaskETA: Scheduled execution time (Unix timestamp)Important: These Are NOT Auth
go
// WRONG: Using headers for auth
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
taskName := r.Header.Get("X-CloudTasks-TaskName")
// WRONG: Anyone can set this header
if taskName == "" {
w.WriteHeader(http.StatusUnauthorized)
return
}
// Process...
}
// An attacker can:
curl -H "X-CloudTasks-TaskName: projects/X/queues/Y/tasks/Z" \
https://api.example.com/webhook
// And pass your "auth check"Use OIDC tokens for authentication, not headers.
Special Case: App Engine
App Engine Task Handlers
App Engine can receive tasks with special headers:
http
X-AppEngine-QueueName: my-queue
X-AppEngine-TaskName: my-task
X-AppEngine-TaskRetryCount: 0
X-AppEngine-TaskExecutionCount: 1Handler must return 200-299 for success.
Setup
go
package main
import (
"net/http"
"appengine"
)
func init() {
http.HandleFunc("/_ah/queue/deferred", handleTask)
}
func handleTask(w http.ResponseWriter, r *http.Request) {
queueName := r.Header.Get("X-AppEngine-QueueName")
taskName := r.Header.Get("X-AppEngine-TaskName")
// Process task
err := processTask(r.Body)
if err != nil {
// 5xx → retry
w.WriteHeader(http.StatusInternalServerError)
return
}
// 200 → success
w.WriteHeader(http.StatusOK)
}Timeout Behavior
Queue-Level Timeout
Default timeout per target:
- App Engine Standard: 10 minutes
- App Engine Flex: 60 minutes
- Cloud Run: depends on Cloud Run timeout (default 60 sec, max 3600 sec)
If handler doesn't respond within timeout:
Dispatch sent: 10:00:00
Timeout: 10 minutes
No response by: 10:10:00
Cloud Tasks: assume failed → retryHandler Responsibility
Handler must complete within timeout:
go
func SlowHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// If task takes 15 minutes, will timeout (if default 10 min)
result := longRunningOperation(ctx)
// Response might not reach Cloud Tasks
// Cloud Tasks will retry
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, result)
}Solution: Use background jobs for long-running work.
go
func Handler(w http.ResponseWriter, r *http.Request) {
// Quick response
task := parseTask(r)
// Enqueue to background job system (e.g., Dataflow, Beam)
err := backgroundJobQueue.Enqueue(task)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
// Return quickly
w.WriteHeader(http.StatusOK)
}Error Handling
Handler Response Code
200-299: SUCCESS (task completed)
300-399: RETRY (suspicious, shouldn't happen)
400: RETRY (bad request, might be transient)
401-403: RETRY (auth issue, token might refresh)
404: RETRY (endpoint might come back online)
429: RETRY (rate limited, backoff)
5xx: RETRY (server error, temporary)
No response / timeout: RETRYWhat Not To Do
go
// WRONG: Returning success even when failed
func Handler(w http.ResponseWriter, r *http.Request) {
err := processPayment()
if err != nil {
// WRONG: Return 200 OK anyway
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"error": "%v"}`, err)
return
}
}
// Cloud Tasks sees 200 → thinks success → doesn't retry
// Payment failed, task deleted, no retryPattern: Fail Fast
go
// CORRECT: Return error status on failure
func Handler(w http.ResponseWriter, r *http.Request) {
err := processPayment()
if err != nil {
// Return error status
if isTransient(err) {
w.WriteHeader(http.StatusServiceUnavailable) // 503
} else {
w.WriteHeader(http.StatusBadRequest) // 400 (won't retry forever)
}
fmt.Fprintf(w, `{"error": "%v"}`, err)
return
}
w.WriteHeader(http.StatusOK)
}Testing
Local Testing
bash
# Simulate Cloud Tasks dispatch
curl -X POST https://localhost:8080/webhook \
-H "X-CloudTasks-QueueName: my-queue" \
-H "X-CloudTasks-TaskName: my-task" \
-H "X-CloudTasks-TaskRetryCount: 0" \
-H "Content-Type: application/json" \
-d '{"id": "12345"}'Integration Testing
go
func TestPaymentWebhook(t *testing.T) {
req := httptest.NewRequest("POST", "/webhook", bytes.NewReader([]byte(`{"id": "12345"}`)))
req.Header.Set("X-CloudTasks-QueueName", "payment")
req.Header.Set("X-CloudTasks-TaskName", "task-1")
req.Header.Set("X-CloudTasks-TaskRetryCount", "0")
w := httptest.NewRecorder()
PaymentHandler(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}