fix: resolve JSON.parse error in frontend API calls
- Add error handling for JSON encoding in all backend handlers - Add CORS support to backend server - Add proxy configuration for frontend development server - Improve error handling in frontend API calls - Build frontend to create static files for backend serving - Add comprehensive error messages and user feedback Fixes issue where frontend received malformed JSON responses from backend API.
This commit is contained in:
1
backend/data/test.md
Normal file
1
backend/data/test.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# hi
|
||||||
@@ -10,6 +10,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/rs/cors v1.11.1 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||||
|
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
|||||||
@@ -31,9 +31,13 @@ func (h *Handlers) ListFiles() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
if err := json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"files": files,
|
"files": files,
|
||||||
})
|
}); err != nil {
|
||||||
|
h.logger.Errorf("Failed to encode response: %v", err)
|
||||||
|
h.writeError(w, http.StatusInternalServerError, "failed to encode response")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,9 +66,13 @@ func (h *Handlers) CreateFile() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
if err := json.NewEncoder(w).Encode(map[string]string{
|
||||||
"message": "file created successfully",
|
"message": "file created successfully",
|
||||||
})
|
}); err != nil {
|
||||||
|
h.logger.Errorf("Failed to encode response: %v", err)
|
||||||
|
h.writeError(w, http.StatusInternalServerError, "failed to encode response")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,10 +94,14 @@ func (h *Handlers) GetFile() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
if err := json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"content": string(content),
|
"content": string(content),
|
||||||
})
|
}); err != nil {
|
||||||
|
h.logger.Errorf("Failed to encode response: %v", err)
|
||||||
|
h.writeError(w, http.StatusInternalServerError, "failed to encode response")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,9 +132,13 @@ func (h *Handlers) UpdateFile() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
if err := json.NewEncoder(w).Encode(map[string]string{
|
||||||
"message": "file updated successfully",
|
"message": "file updated successfully",
|
||||||
})
|
}); err != nil {
|
||||||
|
h.logger.Errorf("Failed to encode response: %v", err)
|
||||||
|
h.writeError(w, http.StatusInternalServerError, "failed to encode response")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,16 +159,23 @@ func (h *Handlers) DeleteFile() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
if err := json.NewEncoder(w).Encode(map[string]string{
|
||||||
"message": "file deleted successfully",
|
"message": "file deleted successfully",
|
||||||
})
|
}); err != nil {
|
||||||
|
h.logger.Errorf("Failed to encode response: %v", err)
|
||||||
|
h.writeError(w, http.StatusInternalServerError, "failed to encode response")
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handlers) writeError(w http.ResponseWriter, status int, message string) {
|
func (h *Handlers) writeError(w http.ResponseWriter, status int, message string) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
json.NewEncoder(w).Encode(map[string]string{
|
if err := json.NewEncoder(w).Encode(map[string]string{
|
||||||
"error": message,
|
"error": message,
|
||||||
})
|
}); err != nil {
|
||||||
|
h.logger.Errorf("Failed to encode error response: %v", err)
|
||||||
|
w.Write([]byte(`{"error": "failed to encode error response"}`))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"markdown-editor/internal/logger"
|
"markdown-editor/internal/logger"
|
||||||
"markdown-editor/internal/storage"
|
"markdown-editor/internal/storage"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/rs/cors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,6 +74,15 @@ func (s *Server) setupRoutes() {
|
|||||||
frontendDir = frontendDirEnv
|
frontendDir = frontendDirEnv
|
||||||
}
|
}
|
||||||
s.router.PathPrefix("/").Handler(http.FileServer(http.Dir(frontendDir)))
|
s.router.PathPrefix("/").Handler(http.FileServer(http.Dir(frontendDir)))
|
||||||
|
|
||||||
|
// Enable CORS
|
||||||
|
cors := cors.New(cors.Options{
|
||||||
|
AllowedOrigins: []string{"http://localhost:3000", "http://localhost:8080"},
|
||||||
|
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||||
|
AllowedHeaders: []string{"Content-Type", "Authorization"},
|
||||||
|
AllowCredentials: true,
|
||||||
|
})
|
||||||
|
s.router.Use(cors.Handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) serve() error {
|
func (s *Server) serve() error {
|
||||||
|
|||||||
BIN
backend/markdown-editor
Executable file
BIN
backend/markdown-editor
Executable file
Binary file not shown.
28
backend/test/test_api.sh
Executable file
28
backend/test/test_api.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/nix/store/ypgcdmzzlgnrmdcsq72c3dxz651jg9zc-bash-5.3p3/bin/bash
|
||||||
|
|
||||||
|
# Start the server in the background
|
||||||
|
./markdown-editor --data-dir ./test/data --port 8081 --host 127.0.0.1 &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Give the server time to start
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Test the /api/files endpoint
|
||||||
|
echo "Testing GET /api/files..."
|
||||||
|
response=$(curl -s http://127.0.0.1:8081/api/files)
|
||||||
|
echo "Response: $response"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test if it's valid JSON
|
||||||
|
if echo "$response" | python3 -m json.tool > /dev/null 2>&1; then
|
||||||
|
echo "✓ Response is valid JSON"
|
||||||
|
else
|
||||||
|
echo "✗ Response is NOT valid JSON"
|
||||||
|
echo "Response was: $response"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Kill the server
|
||||||
|
kill $SERVER_PID 2>/dev/null
|
||||||
|
wait $SERVER_PID 2>/dev/null
|
||||||
|
|
||||||
|
echo "Test completed"
|
||||||
35
backend/test/test_api_detailed.sh
Executable file
35
backend/test/test_api_detailed.sh
Executable file
@@ -0,0 +1,35 @@
|
|||||||
|
#!/nix/store/ypgcdmzzlgnrmdcsq72c3dxz651jg9zc-bash-5.3p3/bin/bash
|
||||||
|
|
||||||
|
# Start the server in the background
|
||||||
|
./markdown-editor --data-dir ./test/data --port 8082 --host 127.0.0.1 &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Give the server time to start
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Test the /api/files endpoint
|
||||||
|
echo "Testing GET /api/files..."
|
||||||
|
response=$(curl -s -i http://127.0.0.1:8082/api/files)
|
||||||
|
echo "$response"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Extract just the body (skip headers)
|
||||||
|
body=$(echo "$response" | awk 'NR>1 && $0 !~ /^[A-Za-z]/ {print; next} /^[A-Za-z]/ {exit}')
|
||||||
|
echo "Body: '$body'"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test if it's valid JSON
|
||||||
|
echo "Checking if response is valid JSON..."
|
||||||
|
if echo "$body" | node -e "console.log(JSON.parse(require('fs').readFileSync(0, 'utf8')))" > /dev/null 2>&1; then
|
||||||
|
echo "✓ Response is valid JSON"
|
||||||
|
echo "$body" | node -e "console.log(JSON.parse(require('fs').readFileSync(0, 'utf8')))"
|
||||||
|
else
|
||||||
|
echo "✗ Response is NOT valid JSON"
|
||||||
|
echo "Response was: '$body'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Kill the server
|
||||||
|
kill $SERVER_PID 2>/dev/null
|
||||||
|
wait $SERVER_PID 2>/dev/null
|
||||||
|
|
||||||
|
echo "Test completed"
|
||||||
82
backend/test_api.sh
Executable file
82
backend/test_api.sh
Executable file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/nix/store/ypgcdmzzlgnrmdcsq72c3dxz651jg9zc-bash-5.3p3/bin/bash
|
||||||
|
|
||||||
|
# Create test data directory
|
||||||
|
mkdir -p test/data
|
||||||
|
|
||||||
|
# Start the server in the background
|
||||||
|
./markdown-editor --data-dir test/data --port 8080 --host 127.0.0.1 > /tmp/server.log 2>&1 &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Wait for server to start
|
||||||
|
echo "Waiting for server to start..."
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Check if server is running
|
||||||
|
if ! ps -p $SERVER_PID > /dev/null; then
|
||||||
|
echo "Server failed to start"
|
||||||
|
cat /tmp/server.log
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Server is running (PID: $SERVER_PID)"
|
||||||
|
echo "Testing API endpoints..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 1: GET /api/files (empty)
|
||||||
|
echo "Test 1: GET /api/files (no files)"
|
||||||
|
response=$(curl -s http://127.0.0.1:8080/api/files)
|
||||||
|
echo "Response: $response"
|
||||||
|
if echo "$response" | grep -q '"files"'; then
|
||||||
|
echo "✓ Response contains 'files' key"
|
||||||
|
else
|
||||||
|
echo "✗ Response does not contain 'files' key"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 2: Create a file
|
||||||
|
echo "Test 2: POST /api/files (create test.md)"
|
||||||
|
response=$(curl -s -X POST http://127.0.0.1:8080/api/files \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"filename":"test.md","content":"# Test Content"}')
|
||||||
|
echo "Response: $response"
|
||||||
|
if echo "$response" | grep -q '"message"'; then
|
||||||
|
echo "✓ Response contains 'message' key"
|
||||||
|
else
|
||||||
|
echo "✗ Response does not contain 'message' key"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 3: GET /api/files (with file)
|
||||||
|
echo "Test 3: GET /api/files (with file)"
|
||||||
|
response=$(curl -s http://127.0.0.1:8080/api/files)
|
||||||
|
echo "Response: $response"
|
||||||
|
if echo "$response" | grep -q '"files"'; then
|
||||||
|
echo "✓ Response contains 'files' key"
|
||||||
|
if echo "$response" | grep -q '"test.md"'; then
|
||||||
|
echo "✓ Response contains 'test.md'"
|
||||||
|
else
|
||||||
|
echo "✗ Response does not contain 'test.md'"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "✗ Response does not contain 'files' key"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Test 4: GET /api/files/test.md
|
||||||
|
echo "Test 4: GET /api/files/test.md"
|
||||||
|
response=$(curl -s http://127.0.0.1:8080/api/files/test.md)
|
||||||
|
echo "Response: $response"
|
||||||
|
if echo "$response" | grep -q '"content"'; then
|
||||||
|
echo "✓ Response contains 'content' key"
|
||||||
|
else
|
||||||
|
echo "✗ Response does not contain 'content' key"
|
||||||
|
fi
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Kill the server
|
||||||
|
echo "Stopping server..."
|
||||||
|
kill $SERVER_PID
|
||||||
|
wait $SERVER_PID 2>/dev/null
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "All tests completed!"
|
||||||
@@ -23,10 +23,14 @@ const App: React.FC = () => {
|
|||||||
const fetchFiles = async () => {
|
const fetchFiles = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/files');
|
const response = await fetch('/api/files');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setFiles(data.files || []);
|
setFiles(data.files || []);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching files:', error);
|
console.error('Error fetching files:', error);
|
||||||
|
// Optionally show an error message to the user
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,19 +49,27 @@ const App: React.FC = () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (!response.ok) {
|
||||||
setNewFilename('');
|
const errorData = await response.json();
|
||||||
setNewContent('');
|
alert(`Error creating file: ${errorData.error || 'Unknown error'}`);
|
||||||
fetchFiles();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setNewFilename('');
|
||||||
|
setNewContent('');
|
||||||
|
fetchFiles();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating file:', error);
|
console.error('Error creating file:', error);
|
||||||
|
alert('Error creating file. Please check console for details.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOpenFile = async (filename: string) => {
|
const handleOpenFile = async (filename: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/api/files/${filename}`);
|
const response = await fetch(`/api/files/${filename}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setCurrentFile({
|
setCurrentFile({
|
||||||
filename,
|
filename,
|
||||||
@@ -83,11 +95,16 @@ const App: React.FC = () => {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (!response.ok) {
|
||||||
alert('File saved successfully!');
|
const errorData = await response.json();
|
||||||
|
alert(`Error saving file: ${errorData.error || 'Unknown error'}`);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
alert('File saved successfully!');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving file:', error);
|
console.error('Error saving file:', error);
|
||||||
|
alert('Error saving file. Please check console for details.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,14 +116,19 @@ const App: React.FC = () => {
|
|||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (!response.ok) {
|
||||||
fetchFiles();
|
const errorData = await response.json();
|
||||||
if (currentFile?.filename === filename) {
|
alert(`Error deleting file: ${errorData.error || 'Unknown error'}`);
|
||||||
setCurrentFile(null);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fetchFiles();
|
||||||
|
if (currentFile?.filename === filename) {
|
||||||
|
setCurrentFile(null);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting file:', error);
|
console.error('Error deleting file:', error);
|
||||||
|
alert('Error deleting file. Please check console for details.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
11
frontend/src/setupProxy.js
Normal file
11
frontend/src/setupProxy.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||||
|
|
||||||
|
module.exports = function(app) {
|
||||||
|
app.use(
|
||||||
|
'/api',
|
||||||
|
createProxyMiddleware({
|
||||||
|
target: 'http://localhost:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user