From 69bb3aea030c2005d69f8f0692dace6561fef969 Mon Sep 17 00:00:00 2001 From: Nicola Zangrandi Date: Fri, 28 Feb 2025 17:28:08 +0100 Subject: [PATCH] fix(feeds): improve OPML import error handling and diagnostics --- feeds/handler.go | 46 ++++++++++++++++++++++ frontend/src/lib/feeds.ts | 82 ++++++++++++++++++++++++++++----------- 2 files changed, 105 insertions(+), 23 deletions(-) diff --git a/feeds/handler.go b/feeds/handler.go index 09ca7ba..318febf 100644 --- a/feeds/handler.go +++ b/feeds/handler.go @@ -2,6 +2,7 @@ package feeds import ( "fmt" + "io" "net/http" "strconv" "strings" @@ -112,25 +113,41 @@ func (h *Handler) handleRefreshAllFeeds(c *gin.Context) { c.Status(http.StatusOK) } +// Helper function to get the minimum of two integers +func min(a, b int) int { + if a < b { + return a + } + return b +} + // handleImportOPML handles POST /api/feeds/import func (h *Handler) handleImportOPML(c *gin.Context) { // Set max file size to 10MB const maxFileSize = 10 << 20 c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxFileSize) + // Log the content type of the request + fmt.Printf("OPML Import - Content-Type: %s\n", c.Request.Header.Get("Content-Type")) + // Parse the multipart form if err := c.Request.ParseMultipartForm(maxFileSize); err != nil { + fmt.Printf("OPML Import - Error parsing multipart form: %v\n", err) 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 { + fmt.Printf("OPML Import - Error getting form file: %v\n", err) + // Check if URL is provided instead url := c.PostForm("url") if url != "" { + fmt.Printf("OPML Import - Using URL instead: %s\n", url) resp, err := http.Get(url) if err != nil { + fmt.Printf("OPML Import - Error downloading from URL: %v\n", err) c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Failed to download OPML: %v", err)}) return } @@ -142,6 +159,7 @@ func (h *Handler) handleImportOPML(c *gin.Context) { feeds, err := h.service.ImportOPML(resp.Body) if err != nil { + fmt.Printf("OPML Import - Error importing from URL: %v\n", err) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } @@ -158,8 +176,13 @@ func (h *Handler) handleImportOPML(c *gin.Context) { } }() + // Log file information + fmt.Printf("OPML Import - File: %s, Size: %d, Content-Type: %s\n", + header.Filename, header.Size, header.Header.Get("Content-Type")) + // Check file size if header.Size > maxFileSize { + fmt.Printf("OPML Import - File too large: %d bytes\n", header.Size) c.JSON(http.StatusBadRequest, gin.H{"error": "File too large (max 10MB)"}) return } @@ -167,16 +190,39 @@ func (h *Handler) handleImportOPML(c *gin.Context) { // Check file extension if !strings.HasSuffix(strings.ToLower(header.Filename), ".opml") && !strings.HasSuffix(strings.ToLower(header.Filename), ".xml") { + fmt.Printf("OPML Import - Invalid file extension: %s\n", header.Filename) c.JSON(http.StatusBadRequest, gin.H{"error": "File must be an OPML or XML file"}) return } + // Try to read a small sample of the file to check if it looks like XML + sampleBuf := make([]byte, 1024) + n, err := file.Read(sampleBuf) + if err != nil && err != io.EOF { + fmt.Printf("OPML Import - Error reading file sample: %v\n", err) + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Error reading file: %v", err)}) + return + } + + // Log the first few bytes of the file + sample := string(sampleBuf[:n]) + fmt.Printf("OPML Import - File sample: %s\n", sample[:min(100, len(sample))]) + + // Reset the file pointer to the beginning + if _, err := file.Seek(0, 0); err != nil { + fmt.Printf("OPML Import - Error resetting file pointer: %v\n", err) + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Error processing file: %v", err)}) + return + } + feeds, err := h.service.ImportOPML(file) if err != nil { + fmt.Printf("OPML Import - Error importing OPML: %v\n", err) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } + fmt.Printf("OPML Import - Successfully imported %d feeds\n", len(feeds)) c.JSON(http.StatusOK, gin.H{"imported": len(feeds)}) } diff --git a/frontend/src/lib/feeds.ts b/frontend/src/lib/feeds.ts index f671888..186c8ae 100644 --- a/frontend/src/lib/feeds.ts +++ b/frontend/src/lib/feeds.ts @@ -117,35 +117,71 @@ function createFeedsStore() { throw new Error('File size exceeds the maximum limit of 10MB'); } + console.log('Importing OPML file:', file.name, 'Size:', file.size, 'Type:', file.type); + const formData = new FormData(); formData.append('file', file); - const response = await fetch('/api/feeds/import', { - method: 'POST', - body: formData - }); + try { + const response = await fetch('/api/feeds/import', { + method: 'POST', + body: formData + }); - // Handle non-OK responses - if (!response.ok) { - 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.` - ); + console.log('OPML import response status:', response.status, response.statusText); + + // Log headers in a type-safe way + const headerLog: Record = {}; + response.headers.forEach((value, key) => { + headerLog[key] = value; + }); + console.log('OPML import response headers:', headerLog); + + // Handle non-OK responses + if (!response.ok) { + const contentType = response.headers.get('content-type'); + console.log('OPML import error content-type:', contentType); + + if (contentType && contentType.includes('application/json')) { + // Try to parse error as JSON + try { + const errorData = await response.json(); + console.log('OPML import error data:', errorData); + throw new Error(errorData.error || `Failed to import OPML: ${response.statusText}`); + } catch (jsonError) { + console.error('Error parsing JSON error response:', jsonError); + throw new Error( + `Failed to import OPML: ${response.statusText} (Error parsing error response)` + ); + } + } else { + // Handle non-JSON responses + try { + const text = await response.text(); + console.log('OPML import error text (first 200 chars):', text.substring(0, 200)); + throw new Error( + `Server returned HTML instead of JSON. Status: ${response.status} ${response.statusText}` + ); + } catch (textError) { + console.error('Error reading response text:', textError); + throw new Error( + `Failed to import OPML: ${response.statusText} (Could not read response)` + ); + } + } } + + const result = await response.json(); + console.log('OPML import success:', result); + + // Reload feeds to get the newly imported ones + await store.load(); + + return result; + } catch (fetchError) { + console.error('OPML import fetch error:', fetchError); + throw fetchError; } - - const result = await response.json(); - - // Reload feeds to get the newly imported ones - await store.load(); - - return result; } catch (error) { console.error('Error importing OPML:', error); throw error;