mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-05 00:43:53 -04:00
feat(provider): vultr.com (#829)
This commit is contained in:
3
.github/workflows/configs/mlc-config.json
vendored
3
.github/workflows/configs/mlc-config.json
vendored
@@ -29,6 +29,9 @@
|
||||
},
|
||||
{
|
||||
"pattern": "^https://www.duckdns.org/$"
|
||||
},
|
||||
{
|
||||
"pattern": "^https://my.vultr.com/settings/#settingsapi$"
|
||||
}
|
||||
],
|
||||
"timeout": "20s",
|
||||
|
||||
@@ -93,6 +93,7 @@ This readme and the [docs/](docs/) directory are **versioned** to match the prog
|
||||
- Spdyn
|
||||
- Strato.de
|
||||
- Variomedia.de
|
||||
- Vultr
|
||||
- Zoneedit
|
||||
- **Want more?** [Create an issue for it](https://github.com/qdm12/ddns-updater/issues/new/choose)!
|
||||
- Web user interface (Desktop)
|
||||
@@ -256,6 +257,7 @@ Check the documentation for your DNS provider:
|
||||
- [Spdyn](docs/spdyn.md)
|
||||
- [Strato.de](docs/strato.md)
|
||||
- [Variomedia.de](docs/variomedia.md)
|
||||
- [Vultr](docs/vultr.md)
|
||||
- [Zoneedit](docs/zoneedit.md)
|
||||
|
||||
Note that:
|
||||
|
||||
30
docs/vultr.md
Normal file
30
docs/vultr.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Vultr
|
||||
|
||||
## Configuration
|
||||
|
||||
### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"settings": [
|
||||
{
|
||||
"provider": "vultr",
|
||||
"domain": "potato.example.com",
|
||||
"apikey": "AAAAAAAAAAAAAAA",
|
||||
"ttl": 300,
|
||||
"ip_version": "ipv4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"` is the domain to update. It can be a root domain (i.e. `example.com`) or a subdomain (i.e. `potato.example.com`) or a wildcard (i.e. `*.example.com`). In case of a wildcard, it only works if there is no existing wildcard records of any record type.
|
||||
- `"apikey"` is your API key which can be obtained from [my.vultr.com/settings/](https://my.vultr.com/settings/#settingsapi).
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`.
|
||||
- `"ipv6_suffix"` is the IPv6 interface identifier suffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating.
|
||||
- `"ttl"` is the record TTL which defaults to 900 seconds.
|
||||
2
go.sum
2
go.sum
@@ -43,8 +43,6 @@ github.com/qdm12/gosettings v0.4.4-rc1 h1:VT+6O6ww3Cn5v5/LgY2zlXoiCkZzbaLDWaA8uf
|
||||
github.com/qdm12/gosettings v0.4.4-rc1/go.mod h1:CPrt2YC4UsURTrslmhxocVhMCW03lIrqdH2hzIf5prg=
|
||||
github.com/qdm12/gosplash v0.2.0 h1:DOxCEizbW6ZG+FgpH2oK1atT6bM8MHL9GZ2ywSS4zZY=
|
||||
github.com/qdm12/gosplash v0.2.0/go.mod h1:k+1PzhO0th9cpX4q2Nneu4xTsndXqrM/x7NTIYmJ4jo=
|
||||
github.com/qdm12/gotree v0.2.0 h1:+58ltxkNLUyHtATFereAcOjBVfY6ETqRex8XK90Fb/c=
|
||||
github.com/qdm12/gotree v0.2.0/go.mod h1:1SdFaqKZuI46U1apbXIf25pDMNnrPuYLEqMF/qL4lY4=
|
||||
github.com/qdm12/gotree v0.3.0 h1:Q9f4C571EFK7ZEsPkEL2oGZX7I+ZhVxhh1ZSydW+5yI=
|
||||
github.com/qdm12/gotree v0.3.0/go.mod h1:iz06uXmRR4Aq9v6tX7mosXStO/yGHxRA1hbyD0UVeYw=
|
||||
github.com/qdm12/log v0.1.0 h1:jYBd/xscHYpblzZAd2kjZp2YmuYHjAAfbTViJWxoPTw=
|
||||
|
||||
@@ -52,6 +52,7 @@ const (
|
||||
Spdyn models.Provider = "spdyn"
|
||||
Strato models.Provider = "strato"
|
||||
Variomedia models.Provider = "variomedia"
|
||||
Vultr models.Provider = "vultr"
|
||||
Zoneedit models.Provider = "zoneedit"
|
||||
)
|
||||
|
||||
@@ -102,6 +103,7 @@ func ProviderChoices() []models.Provider {
|
||||
Spdyn,
|
||||
Strato,
|
||||
Variomedia,
|
||||
Vultr,
|
||||
Zoneedit,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/spdyn"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/strato"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/variomedia"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/vultr"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/providers/zoneedit"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
@@ -177,6 +178,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, owner strin
|
||||
return strato.New(data, domain, owner, ipVersion, ipv6Suffix)
|
||||
case constants.Variomedia:
|
||||
return variomedia.New(data, domain, owner, ipVersion, ipv6Suffix)
|
||||
case constants.Vultr:
|
||||
return vultr.New(data, domain, owner, ipVersion, ipv6Suffix)
|
||||
case constants.Zoneedit:
|
||||
return zoneedit.New(data, domain, owner, ipVersion, ipv6Suffix)
|
||||
default:
|
||||
|
||||
118
internal/provider/providers/vultr/createrecord.go
Normal file
118
internal/provider/providers/vultr/createrecord.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package vultr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/provider/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/utils"
|
||||
)
|
||||
|
||||
// https://www.vultr.com/api/#tag/dns/operation/create-dns-domain-record
|
||||
func (p *Provider) createRecord(ctx context.Context, client *http.Client, ip netip.Addr) (err error) {
|
||||
recordType := constants.A
|
||||
if ip.Is6() {
|
||||
recordType = constants.AAAA
|
||||
}
|
||||
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.vultr.com",
|
||||
Path: fmt.Sprintf("/v2/domains/%s/records", p.domain),
|
||||
}
|
||||
|
||||
requestData := struct {
|
||||
Type string `json:"type"`
|
||||
Data string `json:"data"`
|
||||
Name string `json:"name"`
|
||||
TTL uint32 `json:"ttl,omitempty"`
|
||||
}{
|
||||
Type: recordType,
|
||||
Data: ip.String(),
|
||||
Name: p.owner,
|
||||
TTL: p.ttl,
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
err = encoder.Encode(requestData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("json encoding request data: %w", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating http request: %w", err)
|
||||
}
|
||||
p.setHeaders(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
_ = response.Body.Close()
|
||||
return fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
|
||||
err = response.Body.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing response body: %w", err)
|
||||
}
|
||||
|
||||
switch response.StatusCode {
|
||||
case http.StatusCreated:
|
||||
case http.StatusBadRequest:
|
||||
return fmt.Errorf("%w: %s", errors.ErrBadRequest, parseJSONErrorOrFullBody(bodyBytes))
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return fmt.Errorf("%w: %s", errors.ErrAuth, parseJSONErrorOrFullBody(bodyBytes))
|
||||
case http.StatusNotFound:
|
||||
return fmt.Errorf("%w: %s", errors.ErrDomainNotFound, parseJSONErrorOrFullBody(bodyBytes))
|
||||
default:
|
||||
return fmt.Errorf("%w: %s: %s", errors.ErrHTTPStatusNotValid,
|
||||
response.Status, parseJSONErrorOrFullBody(bodyBytes))
|
||||
}
|
||||
|
||||
errorMessage := parseJSONError(bodyBytes)
|
||||
if errorMessage != "" {
|
||||
return fmt.Errorf("%w: %s", errors.ErrUnsuccessful, errorMessage)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseJSONErrorOrFullBody parses the json error from a response body
|
||||
// and returns it if it is not empty. If the json decoding fails OR
|
||||
// the error parsed is empty, the entire body is returned on a single line.
|
||||
func parseJSONErrorOrFullBody(body []byte) (message string) {
|
||||
var parsedJSON struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
err := json.Unmarshal(body, &parsedJSON)
|
||||
if err != nil || parsedJSON.Error == "" {
|
||||
return utils.ToSingleLine(string(body))
|
||||
}
|
||||
return parsedJSON.Error
|
||||
}
|
||||
|
||||
// parseJSONError parses the json error from a response body
|
||||
// and returns it directly. If the json decoding fails, the
|
||||
// entire body is returned on a single line.
|
||||
func parseJSONError(body []byte) (message string) {
|
||||
var parsedJSON struct {
|
||||
Error string `json:"error"`
|
||||
}
|
||||
err := json.Unmarshal(body, &parsedJSON)
|
||||
if err != nil {
|
||||
return utils.ToSingleLine(string(body))
|
||||
}
|
||||
return parsedJSON.Error
|
||||
}
|
||||
100
internal/provider/providers/vultr/getrecord.go
Normal file
100
internal/provider/providers/vultr/getrecord.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package vultr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
)
|
||||
|
||||
// https://www.vultr.com/api/#tag/dns/operation/list-dns-domain-records
|
||||
func (p *Provider) getRecord(ctx context.Context, client *http.Client,
|
||||
recordType string) (recordID string, recordIP netip.Addr, err error,
|
||||
) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.vultr.com",
|
||||
Path: fmt.Sprintf("/v2/domains/%s/records", p.domain),
|
||||
}
|
||||
|
||||
// max return of get records is 500 records
|
||||
values := url.Values{}
|
||||
values.Set("per_page", "500")
|
||||
u.RawQuery = values.Encode()
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return "", netip.Addr{}, fmt.Errorf("creating http request: %w", err)
|
||||
}
|
||||
p.setHeaders(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return "", netip.Addr{}, err
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
_ = response.Body.Close()
|
||||
return "", netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
|
||||
err = response.Body.Close()
|
||||
if err != nil {
|
||||
return "", netip.Addr{}, fmt.Errorf("closing response body: %w", err)
|
||||
}
|
||||
|
||||
// todo: implement pagination
|
||||
var parsedJSON struct {
|
||||
Error string `json:"error"`
|
||||
Records []struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Data string `json:"data"`
|
||||
} `json:"records"`
|
||||
Meta struct {
|
||||
Total uint32 `json:"total"`
|
||||
Links struct {
|
||||
Next string `json:"next"`
|
||||
Previous string `json:"prev"`
|
||||
} `json:"links"`
|
||||
} `json:"meta"`
|
||||
}
|
||||
err = json.Unmarshal(bodyBytes, &parsedJSON)
|
||||
switch {
|
||||
case err != nil:
|
||||
return "", netip.Addr{}, fmt.Errorf("json decoding response body: %w", err)
|
||||
case response.StatusCode == http.StatusBadRequest:
|
||||
return "", netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrBadRequest, parsedJSON.Error)
|
||||
case response.StatusCode == http.StatusUnauthorized:
|
||||
return "", netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrAuth, parsedJSON.Error)
|
||||
case response.StatusCode == http.StatusNotFound:
|
||||
return "", netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrDomainNotFound, parsedJSON.Error)
|
||||
case response.StatusCode != http.StatusOK:
|
||||
return "", netip.Addr{}, fmt.Errorf("%w: %d: %s",
|
||||
errors.ErrHTTPStatusNotValid, response.StatusCode, parseJSONErrorOrFullBody(bodyBytes))
|
||||
case parsedJSON.Error != "":
|
||||
return "", netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnsuccessful, parsedJSON.Error)
|
||||
}
|
||||
|
||||
// Status is OK (200) and error field is empty
|
||||
|
||||
for _, record := range parsedJSON.Records {
|
||||
if record.Name != p.owner || record.Type != recordType {
|
||||
continue
|
||||
}
|
||||
recordIP, err = netip.ParseAddr(record.Data)
|
||||
if err != nil {
|
||||
return "", netip.Addr{}, fmt.Errorf("parsing existing IP: %w", err)
|
||||
}
|
||||
return record.ID, recordIP, nil
|
||||
}
|
||||
|
||||
return "", netip.Addr{}, fmt.Errorf("%w: in %d records", errors.ErrRecordNotFound, len(parsedJSON.Records))
|
||||
}
|
||||
143
internal/provider/providers/vultr/provider.go
Normal file
143
internal/provider/providers/vultr/provider.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package vultr
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/headers"
|
||||
"github.com/qdm12/ddns-updater/internal/provider/utils"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
domain string
|
||||
owner string
|
||||
ipVersion ipversion.IPVersion
|
||||
ipv6Suffix netip.Prefix
|
||||
apiKey string
|
||||
ttl uint32
|
||||
}
|
||||
|
||||
func New(data json.RawMessage, domain, owner string,
|
||||
ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) (
|
||||
provider *Provider, err error,
|
||||
) {
|
||||
var providerSpecificSettings struct {
|
||||
APIKey string `json:"apikey"`
|
||||
TTL uint32 `json:"ttl"`
|
||||
}
|
||||
err = json.Unmarshal(data, &providerSpecificSettings)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("json decoding provider specific settings: %w", err)
|
||||
}
|
||||
|
||||
err = validateSettings(domain, providerSpecificSettings.APIKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validating provider specific settings: %w", err)
|
||||
}
|
||||
|
||||
return &Provider{
|
||||
domain: domain,
|
||||
owner: owner,
|
||||
ipVersion: ipVersion,
|
||||
ipv6Suffix: ipv6Suffix,
|
||||
apiKey: providerSpecificSettings.APIKey,
|
||||
ttl: providerSpecificSettings.TTL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateSettings(domain, apiKey string) (err error) {
|
||||
err = utils.CheckDomain(domain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", errors.ErrDomainNotValid, err)
|
||||
}
|
||||
|
||||
if apiKey == "" {
|
||||
return fmt.Errorf("%w", errors.ErrAPIKeyNotSet)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provider) String() string {
|
||||
return utils.ToString(p.domain, p.owner, constants.Vultr, p.ipVersion)
|
||||
}
|
||||
|
||||
func (p *Provider) Domain() string {
|
||||
return p.domain
|
||||
}
|
||||
|
||||
func (p *Provider) Owner() string {
|
||||
return p.owner
|
||||
}
|
||||
|
||||
func (p *Provider) IPVersion() ipversion.IPVersion {
|
||||
return p.ipVersion
|
||||
}
|
||||
|
||||
func (p *Provider) IPv6Suffix() netip.Prefix {
|
||||
return p.ipv6Suffix
|
||||
}
|
||||
|
||||
func (p *Provider) Proxied() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Provider) BuildDomainName() string {
|
||||
return utils.BuildDomainName(p.owner, p.domain)
|
||||
}
|
||||
|
||||
func (p *Provider) HTML() models.HTMLRow {
|
||||
return models.HTMLRow{
|
||||
Domain: fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName()),
|
||||
Owner: p.Owner(),
|
||||
Provider: fmt.Sprintf("<a href=\"https://my.vultr.com/dns/edit/?domain=%s#dns-records\">Vultr</a>", p.domain),
|
||||
IPVersion: p.ipVersion.String(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) setHeaders(request *http.Request) {
|
||||
headers.SetUserAgent(request)
|
||||
headers.SetContentType(request, "application/json")
|
||||
headers.SetAccept(request, "application/json")
|
||||
headers.SetAuthBearer(request, p.apiKey)
|
||||
}
|
||||
|
||||
// Update does the following:
|
||||
// 1. if there's no record, create it.
|
||||
// 2. if it exists and ip is different, update it.
|
||||
// 3. if it exists and ip is the same, do nothing.
|
||||
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
|
||||
recordType := constants.A
|
||||
if ip.Is6() {
|
||||
recordType = constants.AAAA
|
||||
}
|
||||
|
||||
recordID, existingIP, err := p.getRecord(ctx, client, recordType)
|
||||
if err != nil {
|
||||
if !stderrors.Is(err, errors.ErrRecordNotFound) {
|
||||
return netip.Addr{}, fmt.Errorf("error getting records for %s: %w", p.domain, err)
|
||||
}
|
||||
err = p.createRecord(ctx, client, ip)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("error creating record: %w", err)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
if existingIP == ip {
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
err = p.updateRecord(ctx, client, recordID, ip)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("error updating record %s: %w", p.BuildDomainName(), err)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
77
internal/provider/providers/vultr/updaterecord.go
Normal file
77
internal/provider/providers/vultr/updaterecord.go
Normal file
@@ -0,0 +1,77 @@
|
||||
package vultr
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
)
|
||||
|
||||
// https://www.vultr.com/api/#tag/dns/operation/update-dns-domain-record
|
||||
func (p *Provider) updateRecord(ctx context.Context, client *http.Client,
|
||||
recordID string, ip netip.Addr,
|
||||
) (err error) {
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "api.vultr.com",
|
||||
Path: fmt.Sprintf("/v2/domains/%s/records/%s", p.domain, recordID),
|
||||
}
|
||||
|
||||
requestData := struct {
|
||||
Data string `json:"data"`
|
||||
Name string `json:"name"`
|
||||
TTL uint32 `json:"ttl,omitempty"`
|
||||
}{
|
||||
Data: ip.String(),
|
||||
Name: p.owner,
|
||||
TTL: p.ttl,
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
err = encoder.Encode(requestData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("json encoding request data: %w", err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPatch, u.String(), buffer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating http request: %w", err)
|
||||
}
|
||||
p.setHeaders(request)
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
_ = response.Body.Close()
|
||||
return fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
|
||||
err = response.Body.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("closing response body: %w", err)
|
||||
}
|
||||
|
||||
if response.StatusCode != http.StatusNoContent {
|
||||
return fmt.Errorf("%w: %d: %s",
|
||||
errors.ErrHTTPStatusNotValid, response.StatusCode,
|
||||
parseJSONErrorOrFullBody(bodyBytes))
|
||||
}
|
||||
|
||||
errorMessage := parseJSONError(bodyBytes)
|
||||
if errorMessage != "" {
|
||||
return fmt.Errorf("%w: %s", errors.ErrUnsuccessful, errorMessage)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user