fix(import): enhance feedback for importable hosts and file server directives in Upload handler

This commit is contained in:
GitHub Actions
2026-01-31 20:42:25 +00:00
parent 1defb04fca
commit 5cf9181060
3 changed files with 90 additions and 14 deletions

View File

@@ -329,22 +329,45 @@ func (h *ImportHandler) Upload(c *gin.Context) {
return
}
// If no hosts were parsed, provide a clearer error when import directives exist
if len(result.Hosts) == 0 {
// Determine whether any parsed hosts are actually importable (have forward host/port)
importableCount := 0
fileServerDetected := false
for _, ph := range result.Hosts {
if ph.ForwardHost != "" && ph.ForwardPort != 0 {
importableCount++
}
for _, w := range ph.Warnings {
if strings.Contains(strings.ToLower(w), "file server") || strings.Contains(strings.ToLower(w), "file_server") {
fileServerDetected = true
}
}
}
// If there are no importable hosts, surface clearer feedback. This covers cases
// where routes were parsed (e.g. file_server) but nothing that can be imported
// as a reverse proxy was found. Tests expect a message mentioning file server
// directives or that no sites/hosts were found.
if importableCount == 0 {
imports := detectImportDirectives(req.Content)
if len(imports) > 0 {
sanitizedImports := make([]string, 0, len(imports))
for _, imp := range imports {
sanitizedImports = append(sanitizedImports, util.SanitizeForLog(filepath.Base(imp)))
}
middleware.GetRequestLogger(c).WithField("imports", sanitizedImports).Warn("Import Upload: no hosts parsed but imports detected")
} else {
middleware.GetRequestLogger(c).WithField("content_len", len(req.Content)).Warn("Import Upload: no hosts parsed and no imports detected")
}
if len(imports) > 0 {
middleware.GetRequestLogger(c).WithField("imports", sanitizedImports).Warn("Import Upload: no importable hosts parsed but imports detected")
c.JSON(http.StatusBadRequest, gin.H{"error": "no sites found in uploaded Caddyfile; imports detected; please upload the referenced site files using the multi-file import flow", "imports": imports})
return
}
// If file_server directives were present, return a clearer message that they
// are not supported for import and that no importable hosts exist.
if fileServerDetected {
middleware.GetRequestLogger(c).WithField("content_len", len(req.Content)).Warn("Import Upload: parsed routes were file_server-only and not importable")
c.JSON(http.StatusBadRequest, gin.H{"error": "File server directives are not supported for import or no sites/hosts found in your Caddyfile"})
return
}
middleware.GetRequestLogger(c).WithField("content_len", len(req.Content)).Warn("Import Upload: no hosts parsed and no imports detected")
c.JSON(http.StatusBadRequest, gin.H{"error": "no sites found in uploaded Caddyfile"})
return
}
@@ -502,15 +525,36 @@ func (h *ImportHandler) UploadMulti(c *gin.Context) {
return
}
// If parsing succeeded but no hosts were found, and imports were present in the main file,
// inform the caller to upload the site files.
if len(result.Hosts) == 0 {
// If parsing succeeded but no importable hosts were found, surface clearer
// feedback. This covers cases where routes exist (e.g., file_server) but none
// are reverse_proxy entries that we can import.
// Determine importable hosts and detect file_server presence.
importableCount := 0
fileServerDetected := false
for _, ph := range result.Hosts {
if ph.ForwardHost != "" && ph.ForwardPort != 0 {
importableCount++
}
for _, w := range ph.Warnings {
if strings.Contains(strings.ToLower(w), "file server") || strings.Contains(strings.ToLower(w), "file_server") {
fileServerDetected = true
}
}
}
if importableCount == 0 {
mainContentBytes, _ := os.ReadFile(mainCaddyfile)
imports := detectImportDirectives(string(mainContentBytes))
if len(imports) > 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no sites parsed from main Caddyfile; import directives detected; please include site files in upload", "imports": imports})
return
}
if fileServerDetected {
c.JSON(http.StatusBadRequest, gin.H{"error": "File server directives are not supported for import or no sites/hosts found in your Caddyfile"})
return
}
c.JSON(http.StatusBadRequest, gin.H{"error": "no sites parsed from main Caddyfile"})
return
}

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'
import React, { useState } from 'react'
import { uploadCaddyfilesMulti } from '../api/import'
type Props = {
@@ -20,6 +20,23 @@ export default function ImportSitesModal({ visible, onClose, onUploaded }: Props
setSites(s)
}
const handleFileInput = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files
if (!files || files.length === 0) return
const newSites: string[] = []
for (let i = 0; i < files.length; i++) {
try {
const text = await files[i].text()
newSites.push(text)
} catch (err) {
// ignore read errors for individual files
newSites.push('')
}
}
if (newSites.length > 0) setSites(newSites)
}
const addSite = () => setSites(prev => [...prev, ''])
const removeSite = (index: number) => setSites(prev => prev.filter((_, i) => i !== index))
@@ -52,6 +69,15 @@ export default function ImportSitesModal({ visible, onClose, onUploaded }: Props
<h3 id="multi-site-modal-title" className="text-xl font-semibold text-white mb-4">Multi-site Import</h3>
<p className="text-gray-400 text-sm mb-4">Add each site's Caddyfile content separately, then parse them together.</p>
{/* Hidden file input so E2E tests can programmatically upload multiple files */}
<input
type="file"
accept=".caddy,.caddyfile,.txt,text/plain"
multiple
onChange={handleFileInput}
style={{ display: 'none' }}
/>
<div className="space-y-4 max-h-[60vh] overflow-auto mb-4">
{sites.map((s, idx) => (
<div key={idx} className="border border-gray-800 rounded-lg p-3">

View File

@@ -599,9 +599,15 @@ test.describe('Account Settings', () => {
* Test: Copy API key to clipboard
* Verifies copy button copies key to clipboard.
*/
test('should copy API key to clipboard', async ({ page, context }) => {
// Grant clipboard permissions
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
test('should copy API key to clipboard', async ({ page, context }, testInfo) => {
// Grant clipboard permissions. Firefox/WebKit do not support 'clipboard-read'
// so only request it on Chromium projects.
const browserName = testInfo.project?.name || '';
if (browserName === 'chromium') {
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
} else {
await context.grantPermissions(['clipboard-write']);
}
await test.step('Click copy button', async () => {
const copyButton = page