mirror of
https://github.com/danielmiessler/Fabric.git
synced 2026-04-24 03:00:15 -04:00
feat: add i18n translations for VertexAI, Gemini, Bedrock, and fetch plugins
- Add VertexAI error message translations across 10 locale files - Add Gemini TTS and audio error translations to all locales - Add AWS Bedrock client error translations to all locales - Add fetch plugin error message translations to all locales - Replace hardcoded English strings with `i18n.T()` calls in Bedrock plugin - Replace hardcoded English strings with `i18n.T()` calls in Gemini plugin - Replace hardcoded English strings with `i18n.T()` calls in VertexAI plugin - Replace hardcoded English strings with `i18n.T()` calls in fetch plugin - Use `errors.New` instead of `fmt.Errorf` for non-formatted error strings
This commit is contained in:
@@ -7,9 +7,11 @@ package bedrock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/domain"
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
"github.com/danielmiessler/fabric/internal/plugins"
|
||||
"github.com/danielmiessler/fabric/internal/plugins/ai"
|
||||
|
||||
@@ -50,11 +52,10 @@ func NewClient() (ret *BedrockClient) {
|
||||
ctx := context.Background()
|
||||
cfg, err := config.LoadDefaultConfig(ctx)
|
||||
if err != nil {
|
||||
// Create a minimal client that will fail gracefully during configuration
|
||||
ret.PluginBase = plugins.NewVendorPluginBase(vendorName, func() error {
|
||||
return fmt.Errorf("unable to load AWS Config: %w", err)
|
||||
return fmt.Errorf(i18n.T("bedrock_unable_load_aws_config"), err)
|
||||
})
|
||||
ret.bedrockRegion = ret.PluginBase.AddSetupQuestion("AWS Region", true)
|
||||
ret.bedrockRegion = ret.PluginBase.AddSetupQuestion(i18n.T("bedrock_aws_region_label"), true)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -68,7 +69,7 @@ func NewClient() (ret *BedrockClient) {
|
||||
ret.runtimeClient = runtimeClient
|
||||
ret.controlPlaneClient = controlPlaneClient
|
||||
|
||||
ret.bedrockRegion = ret.PluginBase.AddSetupQuestion("AWS Region", true)
|
||||
ret.bedrockRegion = ret.PluginBase.AddSetupQuestion(i18n.T("bedrock_aws_region_label"), true)
|
||||
|
||||
if cfg.Region != "" {
|
||||
ret.bedrockRegion.Value = cfg.Region
|
||||
@@ -97,13 +98,13 @@ func (c *BedrockClient) configure() error {
|
||||
|
||||
// Validate region format
|
||||
if !isValidAWSRegion(c.bedrockRegion.Value) {
|
||||
return fmt.Errorf("invalid AWS region: %s", c.bedrockRegion.Value)
|
||||
return fmt.Errorf(i18n.T("bedrock_invalid_aws_region"), c.bedrockRegion.Value)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(c.bedrockRegion.Value))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to load AWS Config with region %s: %w", c.bedrockRegion.Value, err)
|
||||
return fmt.Errorf(i18n.T("bedrock_unable_load_aws_config_with_region"), c.bedrockRegion.Value, err)
|
||||
}
|
||||
|
||||
cfg.APIOptions = append(cfg.APIOptions, middleware.AddUserAgentKeyValue(userAgentKey, userAgentValue))
|
||||
@@ -122,7 +123,7 @@ func (c *BedrockClient) ListModels() ([]string, error) {
|
||||
|
||||
foundationModels, err := c.controlPlaneClient.ListFoundationModels(ctx, &bedrock.ListFoundationModelsInput{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list foundation models: %w", err)
|
||||
return nil, fmt.Errorf(i18n.T("bedrock_failed_list_foundation_models"), err)
|
||||
}
|
||||
|
||||
for _, model := range foundationModels.ModelSummaries {
|
||||
@@ -134,7 +135,7 @@ func (c *BedrockClient) ListModels() ([]string, error) {
|
||||
for inferenceProfilesPaginator.HasMorePages() {
|
||||
inferenceProfiles, err := inferenceProfilesPaginator.NextPage(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list inference profiles: %w", err)
|
||||
return nil, fmt.Errorf(i18n.T("bedrock_failed_list_inference_profiles"), err)
|
||||
}
|
||||
|
||||
for _, profile := range inferenceProfiles.InferenceProfileSummaries {
|
||||
@@ -150,7 +151,7 @@ func (c *BedrockClient) SendStream(msgs []*chat.ChatCompletionMessage, opts *dom
|
||||
// Ensure channel is closed on all exit paths to prevent goroutine leaks
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("panic in SendStream: %v", r)
|
||||
err = fmt.Errorf(i18n.T("bedrock_panic_sendstream"), r)
|
||||
}
|
||||
close(channel)
|
||||
}()
|
||||
@@ -167,7 +168,7 @@ func (c *BedrockClient) SendStream(msgs []*chat.ChatCompletionMessage, opts *dom
|
||||
|
||||
response, err := c.runtimeClient.ConverseStream(context.Background(), &converseInput)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bedrock conversestream failed for model %s: %w", opts.Model, err)
|
||||
return fmt.Errorf(i18n.T("bedrock_conversestream_failed"), opts.Model, err)
|
||||
}
|
||||
|
||||
for event := range response.GetStream().Events() {
|
||||
@@ -209,7 +210,7 @@ func (c *BedrockClient) SendStream(msgs []*chat.ChatCompletionMessage, opts *dom
|
||||
*types.ConverseStreamOutputMemberContentBlockStop:
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unknown stream event type: %T", v)
|
||||
return fmt.Errorf(i18n.T("bedrock_unknown_stream_event_type"), v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,22 +228,22 @@ func (c *BedrockClient) Send(ctx context.Context, msgs []*chat.ChatCompletionMes
|
||||
}
|
||||
response, err := c.runtimeClient.Converse(ctx, &converseInput)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("bedrock converse failed for model %s: %w", opts.Model, err)
|
||||
return "", fmt.Errorf(i18n.T("bedrock_converse_failed"), opts.Model, err)
|
||||
}
|
||||
|
||||
responseText, ok := response.Output.(*types.ConverseOutputMemberMessage)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unexpected response type: %T", response.Output)
|
||||
return "", fmt.Errorf(i18n.T("bedrock_unexpected_response_type"), response.Output)
|
||||
}
|
||||
|
||||
if len(responseText.Value.Content) == 0 {
|
||||
return "", fmt.Errorf("empty response content")
|
||||
return "", errors.New(i18n.T("bedrock_empty_response_content"))
|
||||
}
|
||||
|
||||
responseContentBlock := responseText.Value.Content[0]
|
||||
text, ok := responseContentBlock.(*types.ContentBlockMemberText)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unexpected content block type: %T", responseContentBlock)
|
||||
return "", fmt.Errorf(i18n.T("bedrock_unexpected_content_block_type"), responseContentBlock)
|
||||
}
|
||||
|
||||
return text.Value, nil
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/chat"
|
||||
"github.com/danielmiessler/fabric/internal/domain"
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
"github.com/danielmiessler/fabric/internal/plugins"
|
||||
"github.com/danielmiessler/fabric/internal/plugins/ai/geminicommon"
|
||||
"google.golang.org/genai"
|
||||
@@ -29,7 +31,7 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
errInvalidLocationFormat = "invalid search location format %q: must be timezone (e.g., 'America/Los_Angeles') or language code (e.g., 'en-US')"
|
||||
errInvalidLocationFormat = "gemini_invalid_location_format"
|
||||
locationSeparator = "/"
|
||||
langCodeSeparator = "_"
|
||||
langCodeNormalizedSep = "-"
|
||||
@@ -86,7 +88,7 @@ func (o *Client) Send(ctx context.Context, msgs []*chat.ChatCompletionMessage, o
|
||||
// Check if this is a TTS model request
|
||||
if o.isTTSModel(opts.Model) {
|
||||
if !opts.AudioOutput {
|
||||
err = fmt.Errorf("TTS model '%s' requires audio output. Please specify an audio output file with -o flag ending in .wav", opts.Model)
|
||||
err = fmt.Errorf(i18n.T("tts_model_requires_audio_output"), opts.Model)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -149,7 +151,7 @@ func (o *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.Cha
|
||||
if err != nil {
|
||||
channel <- domain.StreamUpdate{
|
||||
Type: domain.StreamTypeError,
|
||||
Content: fmt.Sprintf("Error: %v", err),
|
||||
Content: fmt.Sprintf(i18n.T("gemini_stream_error"), err),
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -230,7 +232,7 @@ func (o *Client) buildGenerateContentConfig(opts *domain.ChatOptions) (*genai.Ge
|
||||
RetrievalConfig: &genai.RetrievalConfig{LanguageCode: loc},
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf(errInvalidLocationFormat, loc)
|
||||
return nil, fmt.Errorf(i18n.T(errInvalidLocationFormat), loc)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,7 +299,7 @@ func (o *Client) extractTextForTTS(msgs []*chat.ChatCompletionMessage) (string,
|
||||
return msgs[i].Content, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no text content found for TTS generation")
|
||||
return "", errors.New(i18n.T("gemini_no_text_for_tts"))
|
||||
}
|
||||
|
||||
// createGenaiClient creates a new GenAI client for TTS operations
|
||||
@@ -318,7 +320,7 @@ func (o *Client) generateTTSAudio(ctx context.Context, msgs []*chat.ChatCompleti
|
||||
// Validate voice name before making API call
|
||||
if opts.Voice != "" && !IsValidGeminiVoice(opts.Voice) {
|
||||
validVoices := GetGeminiVoiceNames()
|
||||
return "", fmt.Errorf("invalid voice '%s'. Valid voices are: %v", opts.Voice, validVoices)
|
||||
return "", fmt.Errorf(i18n.T("gemini_invalid_voice"), opts.Voice, validVoices)
|
||||
}
|
||||
|
||||
client, err := o.createGenaiClient(ctx)
|
||||
@@ -357,7 +359,7 @@ func (o *Client) performTTSGeneration(ctx context.Context, client *genai.Client,
|
||||
// Generate TTS content
|
||||
response, err := client.Models.GenerateContent(ctx, o.buildModelNameFull(opts.Model), contents, config)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("TTS generation failed: %w", err)
|
||||
return "", fmt.Errorf(i18n.T("gemini_tts_failed"), err)
|
||||
}
|
||||
|
||||
// Extract and process audio data
|
||||
@@ -366,23 +368,23 @@ func (o *Client) performTTSGeneration(ctx context.Context, client *genai.Client,
|
||||
if part.InlineData != nil && len(part.InlineData.Data) > 0 {
|
||||
// Validate audio data format and size
|
||||
if part.InlineData.MIMEType != "" && !strings.HasPrefix(part.InlineData.MIMEType, "audio/") {
|
||||
return "", fmt.Errorf("unexpected data type: %s, expected audio data", part.InlineData.MIMEType)
|
||||
return "", fmt.Errorf(i18n.T("gemini_unexpected_data_type"), part.InlineData.MIMEType)
|
||||
}
|
||||
|
||||
pcmData := part.InlineData.Data
|
||||
if len(pcmData) < MinAudioDataSize {
|
||||
return "", fmt.Errorf("audio data too small: %d bytes, minimum required: %d", len(pcmData), MinAudioDataSize)
|
||||
return "", fmt.Errorf(i18n.T("gemini_audio_data_too_small"), len(pcmData), MinAudioDataSize)
|
||||
}
|
||||
|
||||
// Generate WAV file with proper headers and return the binary data
|
||||
wavData, err := o.generateWAVFile(pcmData)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate WAV file: %w", err)
|
||||
return "", fmt.Errorf(i18n.T("gemini_wav_generation_failed"), err)
|
||||
}
|
||||
|
||||
// Validate generated WAV data
|
||||
if len(wavData) < WAVHeaderSize {
|
||||
return "", fmt.Errorf("generated WAV data is invalid: %d bytes, minimum required: %d", len(wavData), WAVHeaderSize)
|
||||
return "", fmt.Errorf(i18n.T("gemini_wav_data_invalid"), len(wavData), WAVHeaderSize)
|
||||
}
|
||||
|
||||
// Store the binary audio data in a special format that the CLI can detect
|
||||
@@ -391,17 +393,17 @@ func (o *Client) performTTSGeneration(ctx context.Context, client *genai.Client,
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no audio data received from TTS model")
|
||||
return "", errors.New(i18n.T("gemini_no_audio_data"))
|
||||
}
|
||||
|
||||
// generateWAVFile creates WAV data from PCM data with proper headers
|
||||
func (o *Client) generateWAVFile(pcmData []byte) ([]byte, error) {
|
||||
// Validate input size to prevent potential security issues
|
||||
if len(pcmData) == 0 {
|
||||
return nil, fmt.Errorf("empty PCM data provided")
|
||||
return nil, errors.New(i18n.T("gemini_empty_pcm_data"))
|
||||
}
|
||||
if len(pcmData) > MaxAudioDataSize {
|
||||
return nil, fmt.Errorf("PCM data too large: %d bytes, maximum allowed: %d", len(pcmData), MaxAudioDataSize)
|
||||
return nil, fmt.Errorf(i18n.T("gemini_pcm_data_too_large"), len(pcmData), MaxAudioDataSize)
|
||||
}
|
||||
|
||||
// WAV file parameters (Gemini TTS default specs)
|
||||
@@ -444,7 +446,7 @@ func (o *Client) generateWAVFile(pcmData []byte) ([]byte, error) {
|
||||
// Validate generated WAV data
|
||||
result := buf.Bytes()
|
||||
if len(result) < WAVHeaderSize {
|
||||
return nil, fmt.Errorf("generated WAV data is invalid: %d bytes, minimum required: %d", len(result), WAVHeaderSize)
|
||||
return nil, fmt.Errorf(i18n.T("gemini_wav_data_invalid"), len(result), WAVHeaderSize)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
@@ -2,6 +2,7 @@ package vertexai
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -9,6 +10,7 @@ import (
|
||||
"github.com/anthropics/anthropic-sdk-go/vertex"
|
||||
"github.com/danielmiessler/fabric/internal/chat"
|
||||
"github.com/danielmiessler/fabric/internal/domain"
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
debuglog "github.com/danielmiessler/fabric/internal/log"
|
||||
"github.com/danielmiessler/fabric/internal/plugins"
|
||||
"github.com/danielmiessler/fabric/internal/plugins/ai/geminicommon"
|
||||
@@ -65,7 +67,7 @@ func (c *Client) ListModels() ([]string, error) {
|
||||
// Get ADC credentials for API authentication
|
||||
creds, err := google.FindDefaultCredentials(ctx, cloudPlatformScope)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Google credentials (ensure ADC is configured): %w", err)
|
||||
return nil, fmt.Errorf(i18n.T("vertexai_failed_google_credentials"), err)
|
||||
}
|
||||
httpClient := oauth2.NewClient(ctx, creds.TokenSource)
|
||||
|
||||
@@ -104,13 +106,13 @@ func (c *Client) ListModels() ([]string, error) {
|
||||
}
|
||||
|
||||
if len(allModels) == 0 {
|
||||
return nil, fmt.Errorf("no models found from any publisher")
|
||||
return nil, errors.New(i18n.T("vertexai_no_models_found"))
|
||||
}
|
||||
|
||||
// Filter to only conversational models and sort
|
||||
filtered := filterConversationalModels(allModels)
|
||||
if len(filtered) == 0 {
|
||||
return nil, fmt.Errorf("no conversational models found")
|
||||
return nil, errors.New(i18n.T("vertexai_no_conversational_models"))
|
||||
}
|
||||
|
||||
return sortModels(filtered), nil
|
||||
@@ -133,13 +135,13 @@ func getMaxTokens(opts *domain.ChatOptions) int64 {
|
||||
|
||||
func (c *Client) sendClaude(ctx context.Context, msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions) (string, error) {
|
||||
if c.client == nil {
|
||||
return "", fmt.Errorf("VertexAI client not initialized")
|
||||
return "", errors.New(i18n.T("vertexai_client_not_initialized"))
|
||||
}
|
||||
|
||||
// Convert chat messages to Anthropic format
|
||||
anthropicMessages := c.toMessages(msgs)
|
||||
if len(anthropicMessages) == 0 {
|
||||
return "", fmt.Errorf("no valid messages to send")
|
||||
return "", errors.New(i18n.T("vertexai_no_valid_messages"))
|
||||
}
|
||||
|
||||
// Build request params
|
||||
@@ -171,7 +173,7 @@ func (c *Client) sendClaude(ctx context.Context, msgs []*chat.ChatCompletionMess
|
||||
}
|
||||
|
||||
if len(textParts) == 0 {
|
||||
return "", fmt.Errorf("no content in response")
|
||||
return "", errors.New(i18n.T("vertexai_no_content_in_response"))
|
||||
}
|
||||
|
||||
return strings.Join(textParts, ""), nil
|
||||
@@ -187,7 +189,7 @@ func (c *Client) SendStream(msgs []*chat.ChatCompletionMessage, opts *domain.Cha
|
||||
func (c *Client) sendStreamClaude(msgs []*chat.ChatCompletionMessage, opts *domain.ChatOptions, channel chan domain.StreamUpdate) error {
|
||||
if c.client == nil {
|
||||
close(channel)
|
||||
return fmt.Errorf("VertexAI client not initialized")
|
||||
return errors.New(i18n.T("vertexai_client_not_initialized"))
|
||||
}
|
||||
|
||||
defer close(channel)
|
||||
@@ -196,7 +198,7 @@ func (c *Client) sendStreamClaude(msgs []*chat.ChatCompletionMessage, opts *doma
|
||||
// Convert chat messages to Anthropic format
|
||||
anthropicMessages := c.toMessages(msgs)
|
||||
if len(anthropicMessages) == 0 {
|
||||
return fmt.Errorf("no valid messages to send")
|
||||
return errors.New(i18n.T("vertexai_no_valid_messages"))
|
||||
}
|
||||
|
||||
// Build request params
|
||||
@@ -271,12 +273,12 @@ func (c *Client) sendGemini(ctx context.Context, msgs []*chat.ChatCompletionMess
|
||||
Backend: genai.BackendVertexAI,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create Gemini client: %w", err)
|
||||
return "", fmt.Errorf(i18n.T("vertexai_failed_gemini_client"), err)
|
||||
}
|
||||
|
||||
contents := geminicommon.ConvertMessages(msgs)
|
||||
if len(contents) == 0 {
|
||||
return "", fmt.Errorf("no valid messages to send")
|
||||
return "", errors.New(i18n.T("vertexai_no_valid_messages"))
|
||||
}
|
||||
|
||||
config := c.buildGeminiConfig(opts)
|
||||
@@ -345,12 +347,12 @@ func (c *Client) sendStreamGemini(msgs []*chat.ChatCompletionMessage, opts *doma
|
||||
Backend: genai.BackendVertexAI,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Gemini client: %w", err)
|
||||
return fmt.Errorf(i18n.T("vertexai_failed_gemini_client"), err)
|
||||
}
|
||||
|
||||
contents := geminicommon.ConvertMessages(msgs)
|
||||
if len(contents) == 0 {
|
||||
return fmt.Errorf("no valid messages to send")
|
||||
return errors.New(i18n.T("vertexai_no_valid_messages"))
|
||||
}
|
||||
|
||||
config := c.buildGeminiConfig(opts)
|
||||
@@ -361,7 +363,7 @@ func (c *Client) sendStreamGemini(msgs []*chat.ChatCompletionMessage, opts *doma
|
||||
if err != nil {
|
||||
channel <- domain.StreamUpdate{
|
||||
Type: domain.StreamTypeError,
|
||||
Content: fmt.Sprintf("Error: %v", err),
|
||||
Content: fmt.Sprintf(i18n.T("vertexai_stream_error"), err),
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,12 +5,15 @@ package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/danielmiessler/fabric/internal/i18n"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -37,7 +40,7 @@ func (p *FetchPlugin) Apply(operation string, value string) (string, error) {
|
||||
case "get":
|
||||
return p.fetch(value)
|
||||
default:
|
||||
return "", fmt.Errorf("fetch: unknown operation %q (supported: get)", operation)
|
||||
return "", fmt.Errorf(i18n.T("fetch_unknown_operation"), operation)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,11 +72,11 @@ func (p *FetchPlugin) validateTextContent(content []byte) error {
|
||||
debugf("Fetch: validating content length=%d bytes", len(content))
|
||||
|
||||
if !utf8.Valid(content) {
|
||||
return fmt.Errorf("fetch: content is not valid UTF-8 text")
|
||||
return fmt.Errorf(i18n.T("fetch_content_not_utf8"))
|
||||
}
|
||||
|
||||
if bytes.Contains(content, []byte{0}) {
|
||||
return fmt.Errorf("fetch: content contains null bytes")
|
||||
return fmt.Errorf(i18n.T("fetch_content_null_bytes"))
|
||||
}
|
||||
|
||||
debugf("Fetch: content validation successful")
|
||||
@@ -87,42 +90,40 @@ func (p *FetchPlugin) fetch(urlStr string) (string, error) {
|
||||
client := &http.Client{}
|
||||
req, err := http.NewRequest("GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetch: error creating request: %v", err)
|
||||
return "", fmt.Errorf(i18n.T("fetch_error_create_request"), err)
|
||||
}
|
||||
req.Header.Set("User-Agent", UserAgent)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetch: error fetching URL: %v", err)
|
||||
return "", fmt.Errorf(i18n.T("fetch_error_fetching_url"), err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
debugf("Fetch: got response status=%q", resp.Status)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("fetch: HTTP error: %d - %s", resp.StatusCode, resp.Status)
|
||||
return "", fmt.Errorf(i18n.T("fetch_http_error"), resp.StatusCode, resp.Status)
|
||||
}
|
||||
|
||||
if contentLength := resp.ContentLength; contentLength > MaxContentSize {
|
||||
return "", fmt.Errorf("fetch: content too large: %d bytes (max %d bytes)",
|
||||
contentLength, MaxContentSize)
|
||||
return "", fmt.Errorf(i18n.T("fetch_content_too_large"), contentLength, MaxContentSize)
|
||||
}
|
||||
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
debugf("Fetch: content-type=%q", contentType)
|
||||
if !p.isTextContent(contentType) {
|
||||
return "", fmt.Errorf("fetch: unsupported content type %q - only text content allowed",
|
||||
contentType)
|
||||
return "", fmt.Errorf(i18n.T("fetch_unsupported_content_type"), contentType)
|
||||
}
|
||||
|
||||
debugf("Fetch: reading response body")
|
||||
limitReader := io.LimitReader(resp.Body, MaxContentSize+1)
|
||||
content, err := io.ReadAll(limitReader)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("fetch: error reading response: %v", err)
|
||||
return "", fmt.Errorf(i18n.T("fetch_error_reading_response"), err)
|
||||
}
|
||||
|
||||
if len(content) > MaxContentSize {
|
||||
return "", fmt.Errorf("fetch: content too large: exceeds %d bytes", MaxContentSize)
|
||||
return "", fmt.Errorf(i18n.T("fetch_content_exceeds_limit"), MaxContentSize)
|
||||
}
|
||||
|
||||
if err := p.validateTextContent(content); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user