fix(feeds): improve OPML import error handling
This commit is contained in:
parent
7fd939a988
commit
9103cb5d12
3 changed files with 71 additions and 5 deletions
|
@ -4,6 +4,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
@ -113,7 +114,17 @@ func (h *Handler) handleRefreshAllFeeds(c *gin.Context) {
|
||||||
|
|
||||||
// handleImportOPML handles POST /api/feeds/import
|
// handleImportOPML handles POST /api/feeds/import
|
||||||
func (h *Handler) handleImportOPML(c *gin.Context) {
|
func (h *Handler) handleImportOPML(c *gin.Context) {
|
||||||
file, _, err := c.Request.FormFile("file")
|
// Set max file size to 10MB
|
||||||
|
const maxFileSize = 10 << 20
|
||||||
|
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxFileSize)
|
||||||
|
|
||||||
|
// Parse the multipart form
|
||||||
|
if err := c.Request.ParseMultipartForm(maxFileSize); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("File too large or invalid form: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
file, header, err := c.Request.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Check if URL is provided instead
|
// Check if URL is provided instead
|
||||||
url := c.PostForm("url")
|
url := c.PostForm("url")
|
||||||
|
@ -131,7 +142,7 @@ func (h *Handler) handleImportOPML(c *gin.Context) {
|
||||||
|
|
||||||
feeds, err := h.service.ImportOPML(resp.Body)
|
feeds, err := h.service.ImportOPML(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{"imported": len(feeds)})
|
c.JSON(http.StatusOK, gin.H{"imported": len(feeds)})
|
||||||
|
@ -147,9 +158,22 @@ func (h *Handler) handleImportOPML(c *gin.Context) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
// Check file size
|
||||||
|
if header.Size > maxFileSize {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "File too large (max 10MB)"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file extension
|
||||||
|
if !strings.HasSuffix(strings.ToLower(header.Filename), ".opml") &&
|
||||||
|
!strings.HasSuffix(strings.ToLower(header.Filename), ".xml") {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "File must be an OPML or XML file"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
feeds, err := h.service.ImportOPML(file)
|
feeds, err := h.service.ImportOPML(file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-co-op/gocron"
|
"github.com/go-co-op/gocron"
|
||||||
|
@ -77,8 +78,24 @@ func (s *Service) AddFeed(url string) (*Feed, error) {
|
||||||
|
|
||||||
// ImportOPML imports feeds from an OPML file
|
// ImportOPML imports feeds from an OPML file
|
||||||
func (s *Service) ImportOPML(opmlContent io.Reader) ([]Feed, error) {
|
func (s *Service) ImportOPML(opmlContent io.Reader) ([]Feed, error) {
|
||||||
|
// Read the content into a buffer so we can inspect it if there's an error
|
||||||
|
content, err := io.ReadAll(opmlContent)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read OPML content: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the content looks like XML
|
||||||
|
trimmedContent := strings.TrimSpace(string(content))
|
||||||
|
if !strings.HasPrefix(trimmedContent, "<?xml") && !strings.HasPrefix(trimmedContent, "<opml") {
|
||||||
|
// Try to provide a helpful error message
|
||||||
|
if strings.HasPrefix(trimmedContent, "<!DOCTYPE") || strings.HasPrefix(trimmedContent, "<html") {
|
||||||
|
return nil, fmt.Errorf("received HTML instead of OPML XML. Please check that the file is a valid OPML file")
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("content does not appear to be valid OPML XML")
|
||||||
|
}
|
||||||
|
|
||||||
var opml OPML
|
var opml OPML
|
||||||
if err := xml.NewDecoder(opmlContent).Decode(&opml); err != nil {
|
if err := xml.Unmarshal(content, &opml); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse OPML: %w", err)
|
return nil, fmt.Errorf("failed to parse OPML: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -103,6 +103,20 @@ function createFeedsStore() {
|
||||||
},
|
},
|
||||||
importOPML: async (file: File): Promise<{ imported: number }> => {
|
importOPML: async (file: File): Promise<{ imported: number }> => {
|
||||||
try {
|
try {
|
||||||
|
// Validate file type
|
||||||
|
if (
|
||||||
|
!file.name.toLowerCase().endsWith('.opml') &&
|
||||||
|
!file.name.toLowerCase().endsWith('.xml')
|
||||||
|
) {
|
||||||
|
throw new Error('Please select a valid OPML or XML file');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file size (max 10MB)
|
||||||
|
const maxSize = 10 * 1024 * 1024; // 10MB
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
throw new Error('File size exceeds the maximum limit of 10MB');
|
||||||
|
}
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
|
@ -111,8 +125,19 @@ function createFeedsStore() {
|
||||||
body: formData
|
body: formData
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle non-OK responses
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to import OPML: ${response.statusText}`);
|
const contentType = response.headers.get('content-type');
|
||||||
|
if (contentType && contentType.includes('application/json')) {
|
||||||
|
// Try to parse error as JSON
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(errorData.error || `Failed to import OPML: ${response.statusText}`);
|
||||||
|
} else {
|
||||||
|
// Handle non-JSON responses
|
||||||
|
throw new Error(
|
||||||
|
`Failed to import OPML: ${response.statusText}. Server response was not JSON.`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
|
|
Loading…
Add table
Reference in a new issue