mirror of
https://github.com/lukaszraczylo/go-simple-graphql.git
synced 2026-10-12 00:45:31 +00:00
266 lines
7.5 KiB
Go
266 lines
7.5 KiB
Go
package gql
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/avast/retry-go/v4"
|
|
"github.com/goccy/go-json"
|
|
libpack_logger "github.com/lukaszraczylo/go-simple-graphql/logging"
|
|
)
|
|
|
|
var (
|
|
// Shared HTTP transport with optimized settings
|
|
defaultTransport = &http.Transport{
|
|
MaxIdleConns: 100,
|
|
MaxIdleConnsPerHost: 100,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
DisableCompression: true, // Disable automatic compression to handle gzip manually
|
|
ForceAttemptHTTP2: true,
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // Skip TLS verification for test environments
|
|
}
|
|
|
|
// Shared HTTP client with timeouts
|
|
defaultClient = &http.Client{
|
|
Transport: defaultTransport,
|
|
Timeout: 30 * time.Second,
|
|
}
|
|
)
|
|
|
|
func (qe *QueryExecutor) executeQuery() ([]byte, error) {
|
|
// Reuse buffer from pool to avoid allocations
|
|
buf := bufferPool.Get().(*bytes.Buffer)
|
|
defer bufferPool.Put(buf)
|
|
buf.Reset()
|
|
|
|
buf.Write(qe.Query)
|
|
|
|
httpRequest, err := http.NewRequest(http.MethodPost, qe.endpoint, nil)
|
|
if err != nil {
|
|
qe.Logger.Error(&libpack_logger.LogMessage{
|
|
Message: "Can't create HTTP request",
|
|
Pairs: map[string]interface{}{"error": err.Error()},
|
|
})
|
|
return nil, fmt.Errorf("can't create HTTP request: %w", err)
|
|
}
|
|
|
|
for key, value := range qe.Headers {
|
|
httpRequest.Header.Set(key, fmt.Sprint(value))
|
|
}
|
|
// Set Content-Type header if not already set
|
|
if httpRequest.Header.Get("Content-Type") == "" {
|
|
httpRequest.Header.Set("Content-Type", "application/json")
|
|
}
|
|
|
|
// Set Accept-Encoding header to request gzip compression
|
|
if httpRequest.Header.Get("Accept-Encoding") == "" {
|
|
httpRequest.Header.Set("Accept-Encoding", "gzip")
|
|
}
|
|
|
|
retriesMax := 1
|
|
if qe.Retries {
|
|
retriesMax = qe.retries_number
|
|
}
|
|
|
|
var queryResult queryResults
|
|
err = retry.Do(
|
|
func() error {
|
|
// Reset buffer before each retry
|
|
buf.Reset()
|
|
buf.Write(qe.Query)
|
|
|
|
// Set the body of the request
|
|
httpRequest.Body = io.NopCloser(bytes.NewReader(buf.Bytes()))
|
|
httpRequest.ContentLength = int64(buf.Len())
|
|
|
|
// Use default client if custom client is not set
|
|
client := qe.client
|
|
if client == nil {
|
|
client = defaultClient
|
|
}
|
|
httpResponse, err := client.Do(httpRequest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
io.Copy(io.Discard, httpResponse.Body)
|
|
httpResponse.Body.Close()
|
|
}()
|
|
|
|
if httpResponse.StatusCode < http.StatusOK || httpResponse.StatusCode >= http.StatusMultipleChoices {
|
|
return fmt.Errorf("HTTP error - status code: %s for %s", httpResponse.Status, httpRequest.URL)
|
|
}
|
|
|
|
// Log Content-Encoding header for debugging
|
|
encoding := httpResponse.Header.Get("Content-Encoding")
|
|
qe.Logger.Debug(&libpack_logger.LogMessage{
|
|
Message: "Processing response",
|
|
Pairs: map[string]interface{}{
|
|
"content_encoding": encoding,
|
|
"content_type": httpResponse.Header.Get("Content-Type"),
|
|
},
|
|
})
|
|
|
|
// First, read the entire response body into a buffer
|
|
rawBuf := bufferPool.Get().(*bytes.Buffer)
|
|
rawBuf.Reset()
|
|
defer bufferPool.Put(rawBuf)
|
|
|
|
_, err = io.Copy(rawBuf, httpResponse.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading HTTP response body: %w", err)
|
|
}
|
|
|
|
rawData := rawBuf.Bytes()
|
|
qe.Logger.Debug(&libpack_logger.LogMessage{
|
|
Message: "Read raw response data",
|
|
Pairs: map[string]interface{}{
|
|
"response_size": len(rawData),
|
|
},
|
|
})
|
|
|
|
// Use buffer pool for final data processing
|
|
respBuf := bufferPool.Get().(*bytes.Buffer)
|
|
respBuf.Reset()
|
|
defer bufferPool.Put(respBuf)
|
|
|
|
var finalData []byte
|
|
|
|
// Handle gzip decompression with fallback
|
|
if encoding == "gzip" {
|
|
// Try to create gzip reader from raw data
|
|
gzipReader, err := gzip.NewReader(bytes.NewReader(rawData))
|
|
if err != nil {
|
|
qe.Logger.Warning(&libpack_logger.LogMessage{
|
|
Message: "Failed to create gzip reader, falling back to uncompressed parsing",
|
|
Pairs: map[string]interface{}{"gzip_error": err.Error()},
|
|
})
|
|
// Fallback: treat raw data as uncompressed
|
|
finalData = rawData
|
|
} else {
|
|
// Successfully created gzip reader, now try to decompress
|
|
defer gzipReader.Close()
|
|
|
|
_, copyErr := io.Copy(respBuf, gzipReader)
|
|
if copyErr != nil {
|
|
// Gzip decompression failed, try fallback
|
|
qe.Logger.Warning(&libpack_logger.LogMessage{
|
|
Message: "Gzip decompression failed, falling back to uncompressed parsing",
|
|
Pairs: map[string]interface{}{
|
|
"copy_error": copyErr,
|
|
"raw_size": len(rawData),
|
|
},
|
|
})
|
|
// Fallback: treat raw data as uncompressed
|
|
finalData = rawData
|
|
} else {
|
|
// Successful decompression
|
|
finalData = respBuf.Bytes()
|
|
qe.Logger.Debug(&libpack_logger.LogMessage{
|
|
Message: "Successfully decompressed gzip response",
|
|
Pairs: map[string]interface{}{
|
|
"compressed_size": len(rawData),
|
|
"decompressed_size": len(finalData),
|
|
},
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
// No compression, use raw data directly
|
|
finalData = rawData
|
|
}
|
|
|
|
// Validate that we have data before attempting JSON unmarshaling
|
|
if len(finalData) == 0 {
|
|
return fmt.Errorf("empty response data after processing")
|
|
}
|
|
|
|
// Log the first few bytes for debugging (truncated for safety)
|
|
debugData := finalData
|
|
if len(debugData) > 100 {
|
|
debugData = debugData[:100]
|
|
}
|
|
qe.Logger.Debug(&libpack_logger.LogMessage{
|
|
Message: "Attempting JSON unmarshaling",
|
|
Pairs: map[string]interface{}{
|
|
"data_size": len(finalData),
|
|
"data_sample": string(debugData),
|
|
},
|
|
})
|
|
|
|
// Unmarshal the final processed data
|
|
err = json.Unmarshal(finalData, &queryResult)
|
|
if err != nil {
|
|
qe.Logger.Error(&libpack_logger.LogMessage{
|
|
Message: "JSON unmarshaling failed",
|
|
Pairs: map[string]interface{}{
|
|
"error": err.Error(),
|
|
"data_size": len(finalData),
|
|
"data_sample": string(debugData),
|
|
"encoding": encoding,
|
|
},
|
|
})
|
|
return fmt.Errorf("error unmarshalling HTTP response: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
retry.OnRetry(func(n uint, err error) {
|
|
qe.Logger.Warning(&libpack_logger.LogMessage{
|
|
Message: "Retrying query",
|
|
Pairs: map[string]interface{}{"error": err.Error(), "attempt": n + 1},
|
|
})
|
|
}),
|
|
retry.Attempts(uint(retriesMax)),
|
|
retry.DelayType(retry.BackOffDelay),
|
|
retry.Delay(time.Duration(qe.retries_delay)),
|
|
retry.MaxDelay(10*time.Second),
|
|
retry.LastErrorOnly(true),
|
|
)
|
|
if err != nil {
|
|
qe.Logger.Debug(&libpack_logger.LogMessage{
|
|
Message: "Error executing HTTP request",
|
|
Pairs: map[string]interface{}{"error": err.Error()},
|
|
})
|
|
return nil, err
|
|
}
|
|
|
|
if len(queryResult.Errors) > 0 {
|
|
qe.Logger.Debug(&libpack_logger.LogMessage{
|
|
Message: "Error executing query",
|
|
Pairs: map[string]interface{}{"error": queryResult.Errors},
|
|
})
|
|
return nil, fmt.Errorf("error executing query: %s", queryResult.Errors)
|
|
}
|
|
|
|
if queryResult.Data == nil {
|
|
qe.Logger.Debug(&libpack_logger.LogMessage{
|
|
Message: "Error executing query",
|
|
Pairs: map[string]interface{}{"error": "no data"},
|
|
})
|
|
return nil, errors.New("error executing query: no data")
|
|
}
|
|
|
|
// Marshal queryResult.Data
|
|
jsonData, err := json.Marshal(queryResult.Data)
|
|
if err != nil {
|
|
qe.Logger.Debug(&libpack_logger.LogMessage{
|
|
Message: "Error marshalling query result",
|
|
Pairs: map[string]interface{}{"error": err.Error(), "data": queryResult.Data},
|
|
})
|
|
return nil, fmt.Errorf("error marshalling query result: %w. Data: %s", err, queryResult.Data)
|
|
}
|
|
|
|
if qe.CacheKey != "no-cache" {
|
|
qe.cache.Set(qe.CacheKey, jsonData, qe.CacheTTL)
|
|
}
|
|
|
|
return jsonData, nil
|
|
}
|