Files
go-onlyoffice/request.go
T
eSlider d6b8eb7777
Release Please / Release Please (push) Skipped
Release / GoReleaser (push) Skipped
Tests / Secret scan (gitleaks) (push) Skipped
Tests / Test (Go 1.25) (push) Skipped
Tests / Test (Go stable) (push) Skipped
Tests / Secret scan (gitleaks) (pull_request) Successful in 4s
Tests / Test (Go 1.25) (pull_request) Successful in 21s
Tests / Test (Go stable) (pull_request) Successful in 21s
fix(retry): retry transient edge answers centrally, fix stale task test (#57)
- Route HTTP helpers (getJSON/formRequest/deleteReq/postJSON/putJSON/
  multipart upload), Query and AuthenticateContext/ensureToken through the
  deterministic 429/502/503/504 retry (retryRaw), so the integration suite
  no longer fails on the shared openresty rate limit under parallel runs.
- Fix stale cmd/office/fetch integration test: loader.TaskFields was
  replaced by loader.DetailForm (broke go vet -tags=integration).
- Add unit tests for Transient/DoRetry.
2026-09-16 21:32:14 +00:00

203 lines
5.9 KiB
Go

package onlyoffice
// Low-level Request/Query primitive used by the typed helpers in this
// package. Prefer the domain-specific methods (CreateProject, GetTasks, …)
// or the untyped helpers in http.go (ResponseArray, ResponseObject, …) — this
// file is kept for API compatibility and for rare callers that want to hit
// arbitrary endpoints with arbitrary Params/Body.
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/google/go-querystring/query"
)
// Request for OnlyOffice API.
type Request struct {
Uri string // URI is the path to the API endpoint e.g. /api/2.0/project.json
Method string // HTTP method; defaults to GET when empty.
Params any // Params is serialised to the query string via go-querystring.
Body any // Body is marshalled to JSON unless it is []byte or string.
Token *string // Explicit Authorization header; overrides the cached token.
NoAuth bool // Skip automatic authentication.
Debug bool // Kept for backwards compatibility; no longer affects behaviour.
}
// GetMethod returns Method, defaulting to GET when unset.
func (r Request) GetMethod() string {
if r.Method == "" {
return http.MethodGet
}
return r.Method
}
// Query the OnlyOffice API.
//
// - If request.Method is empty it defaults to GET.
// - If request.Body is non-nil it is marshalled to JSON (unless it is
// already []byte or string, which are passed through verbatim).
// - If request.Token is nil the cached session token is used (and
// refreshed as needed unless NoAuth is set).
// - If request.Token is non-nil it is used verbatim as the Authorization
// header.
// - If request.NoAuth is true then no token is fetched — the caller is
// responsible for authenticating requests (used internally by Auth()).
func (c *Client) Query(request Request, result interface{}) error {
return DoRetry(context.Background(), DefaultRetryPolicy(), func() error {
return c.queryOnce(request, result)
})
}
func (c *Client) queryOnce(request Request, result interface{}) error {
url := c.credentials.Url + request.Uri
if request.Params != nil {
v, err := query.Values(request.Params)
if err != nil {
return err
}
url = fmt.Sprintf("%s?%s", url, v.Encode())
}
rdr, err := requestBodyReader(request.Body)
if err != nil {
return err
}
req, err := http.NewRequest(request.GetMethod(), url, rdr)
if err != nil {
return err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Pragma", "no-cache")
if !request.NoAuth {
if err := c.ensureToken(); err != nil {
return fmt.Errorf("failed to authenticate: %w", err)
}
}
switch {
case request.Token != nil:
req.Header.Set("Authorization", *request.Token)
case c.token != nil:
req.Header.Set("Authorization", c.token.Value)
}
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode >= 400 {
return fmt.Errorf("%s %s: %d %s", request.GetMethod(), request.Uri, resp.StatusCode, truncate(string(raw), 400))
}
if result == nil {
return nil
}
return json.Unmarshal(raw, result)
}
// requestBodyReader normalises Query() body input into an io.Reader.
// []byte and string are passed through verbatim; everything else is
// marshalled to JSON. Returns (nil, nil) for a nil body.
func requestBodyReader(body any) (io.Reader, error) {
if body == nil {
return nil, nil
}
switch b := body.(type) {
case []byte:
return bytes.NewReader(b), nil
case string:
return strings.NewReader(b), nil
default:
j, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
return bytes.NewReader(j), nil
}
}
// ToJson for Credentials returns a JSON-encoded payload suitable for the
// authentication endpoint. Returns nil on marshal error (which never happens
// for this struct).
func (c Credentials) ToJson() []byte {
b, err := json.Marshal(c)
if err != nil {
return nil
}
return b
}
// MetaResponse is the shape of every envelope returned by the OnlyOffice API.
// It is embedded inline into the per-endpoint response structs via the
// `json:",inline"` convention used throughout this package.
type MetaResponse struct {
Count int `json:"count"`
Total int `json:"total"`
Status int `json:"status"`
StatusCode int `json:"statusCode"`
}
// Permissions is a common subset of boolean permissions embedded into
// several entity types.
type Permissions struct {
CanEdit *bool `json:"canEdit,omitempty"`
CanDelete *bool `json:"canDelete,omitempty"`
}
// Token is the authentication token returned by /api/2.0/authentication.json.
type Token struct {
Value string `json:"token"`
Expires Time `json:"expires"`
}
// Time wraps time.Time with the OnlyOffice "yyyy-MM-ddTHH:mm:ss.fffffffzzz"
// wire format used by several endpoints.
type Time time.Time
// String formats the time in OnlyOffice's expected ISO-8601 variant (without
// fractional seconds or timezone).
func (t Time) String() string {
return time.Time(t).Format("2006-01-02T15:04:05")
}
// Before reports whether t is before u.
func (t Time) Before(u Time) bool {
return time.Time(t).Before(time.Time(u))
}
// After reports whether t is after u.
func (t Time) After(u Time) bool {
return time.Time(t).After(time.Time(u))
}
// UnmarshalJSON decodes the OnlyOffice wire format into Time.
func (r *Time) UnmarshalJSON(data []byte) error {
data = data[1 : len(data)-1] // trim surrounding quotes
t, err := time.Parse("2006-01-02T15:04:05.0000000-07:00", string(data))
if err != nil {
return err
}
*r = Time(t)
return nil
}
// MarshalJSON emits the OnlyOffice-friendly short form.
func (t Time) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, time.Time(t).Format("2006-01-02T15:04:05"))), nil
}