| 476 | } |
| 477 | |
| 478 | func publishReview(apiOrigin string, payloadJSON []byte) (publishResponse, error) { |
| 479 | origin := strings.TrimRight(strings.TrimSpace(apiOrigin), "/") |
| 480 | if origin == "" { |
| 481 | return publishResponse{}, errors.New("api origin is required") |
| 482 | } |
| 483 | |
| 484 | envelopeBytes, err := json.Marshal(publishEnvelope{ |
| 485 | Content: string(payloadJSON), |
| 486 | }) |
| 487 | if err != nil { |
| 488 | return publishResponse{}, fmt.Errorf("encode publish envelope: %w", err) |
| 489 | } |
| 490 | |
| 491 | req, err := http.NewRequest(http.MethodPost, origin+"/api/publish", bytes.NewReader(envelopeBytes)) |
| 492 | if err != nil { |
| 493 | return publishResponse{}, fmt.Errorf("create publish request: %w", err) |
| 494 | } |
| 495 | req.Header.Set("content-type", "application/json") |
| 496 | req.Header.Set("accept", "application/json") |
| 497 | |
| 498 | client := &http.Client{Timeout: 20 * time.Second} |
| 499 | res, err := client.Do(req) |
| 500 | if err != nil { |
| 501 | return publishResponse{}, fmt.Errorf("failed to reach %s/api/publish: %w", origin, err) |
| 502 | } |
| 503 | defer res.Body.Close() |
| 504 | |
| 505 | body, err := io.ReadAll(io.LimitReader(res.Body, 2<<20)) |
| 506 | if err != nil { |
| 507 | return publishResponse{}, fmt.Errorf("read publish response: %w", err) |
| 508 | } |
| 509 | |
| 510 | if res.StatusCode < 200 || res.StatusCode >= 300 { |
| 511 | return publishResponse{}, fmt.Errorf("publish failed (%d): %s", res.StatusCode, strings.TrimSpace(string(body))) |
| 512 | } |
| 513 | |
| 514 | var response publishResponse |
| 515 | if err := json.Unmarshal(body, &response); err != nil { |
| 516 | return publishResponse{}, fmt.Errorf("decode publish response: %w", err) |
| 517 | } |
| 518 | if strings.TrimSpace(response.Hash) == "" { |
| 519 | return publishResponse{}, fmt.Errorf("publish response missing hash: %s", strings.TrimSpace(string(body))) |
| 520 | } |
| 521 | if strings.TrimSpace(response.URL) == "" { |
| 522 | response.URL = origin + "/" + strings.TrimSpace(response.Hash) |
| 523 | } |
| 524 | |
| 525 | return response, nil |
| 526 | } |
| 527 | |
| 528 | func canonicalShareURL(hash, reviewURL string) string { |
| 529 | trimmedHash := strings.TrimSpace(hash) |