Rails7 原生JS调用写接口报unexpected end of JSON input错误
你遇到的uncaught (in promise) syntax error: unexpected end of JSON input本质原因非常明确:前端调用res.json()解析接口响应时,拿到的根本不是合法JSON格式内容,甚至是空响应体,JSON解析直接失败。
你的项目同时存在后端路由/参数配置错误、前端JS逻辑bug两类问题,按下面步骤改完就能正常跑:
现存问题点
- 路由配置缺失+冲突:你写了
devise_for :users会默认生成POST/users的注册路由,指向Devise自带的控制器,但你自己的路由只配置了GET/users到UsersController#index,根本没定义POST/users、PUT/DELETE/users/:id的路由指向你写的UsersController。非GET请求要么被Devise拦截返回302跳转响应(无响应体),要么被Rails默认CSRF防护拦截返回空错误响应,根本没走到你写的业务逻辑。 - 控制器逻辑不全:你的UsersController没写create动作,参数白名单漏了
email、password_confirmation字段;update动作失败时用head :unprocessable_entity只返回状态码,完全没有响应体,前端硬解析JSON必然报错;set_user用find_by_id找不到用户时会返回nil,后续操作会报500错误。 - 前端JS逻辑漏洞:
- 页面加载初始阶段就把四个输入框的value存成了常量,此时输入框全是空值,用户填完内容提交时,发出去的永远是空参数。
- Promise链写崩:第一个then里
console.log(res.json())没有returnres.json()返回的Promise,后续then拿到的全是undefined;连续写了两个then处理响应,逻辑完全断裂;最后调用renderUsers既没传参数也没加括号执行,根本不会运行。 - 没有做响应状态和格式校验,不管接口返回什么内容(302跳转、无内容状态码、404/500错误页)都直接调用
res.json(),只要响应不是合法JSON就会抛你看到的错。 - 渲染列表的output用了全局变量,每次渲染不会清空,会重复拼接内容;提交的参数没有按后端要求包在
user根键下,后端强校验params.require(:user)会直接报参数缺失错误。
- CSRF防护拦截:Rails默认对非GET请求校验CSRF token,跨域的API请求没带token会被直接拦截,返回空响应。
修复步骤
1. 修正路由配置
替换config/routes.rb里的用户相关路由,统一用RESTful风格,避免和Devise路由冲突:
Rails.application.routes.draw do # 如果不需要用Devise自带的注册路由,加skip: [:registrations],避免路由冲突 devise_for :users, skip: [:registrations] # 一次性生成用户CRUD的标准路由 resources :users, only: [:index, :show, :create, :update, :destroy] end
改完可以在项目目录跑rails routes确认,POST /users、PUT/PATCH /users/:id、DELETE /users/:id都指向UsersController对应动作。
2. 修正控制器逻辑
首先在app/controllers/application_controller.rb里跳过API请求的CSRF校验:
class ApplicationController < ActionController::Base skip_before_action :verify_authenticity_token, if: -> { request.format.json? } end
然后补全、修正UsersController的代码:
class UsersController < ApplicationController before_action :set_user, only: [:show, :update, :destroy] # 删掉没用的edit动作,API接口不需要渲染前端编辑页 def index @users = User.all render json: @users end # 补上缺失的create动作 def create @user = User.new(user_params) if @user.save render json: @user, status: :created else # 不要用head返回空响应,返回带错误信息的JSON render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity end end def update if @user.update(user_params) render json: { message: "更新用户成功", user: @user } else render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity end end def show render json: @user end def destroy @user.destroy render json: { message: "用户已成功删除" } end private def set_user # 用find代替find_by_id,找不到用户时自动返回404状态,不会触发nil错误 @user = User.find(params[:id]) end def user_params # 补上缺失的email、password_confirmation白名单字段 params.require(:user).permit(:name, :email, :position, :phone_number, :image, :password, :password_confirmation) end end
3. 重写前端JS逻辑
替换main.js的全部内容,修复所有逻辑漏洞:
const userList = document.querySelector('.user-lists'); const addUserForm = document.querySelector('.add-user-form'); const baseUrl = 'http://localhost:3000/users'; // 渲染用户列表 const renderUsers = (users) => { // output放到函数内部,每次渲染先清空,避免重复拼接 let output = ''; users.forEach(user => { output += ` <tr> <th scope="row">${user.id}</th> <td>${user.name || '-'}</td> <td>${user.email || '-'}</td> <td>${user.position || '-'}</td> <td>${user.phone_number || '-'}</td> <td>${user.image || '-'}</td> <td> <button class="btn btn-primary edit-btn" data-id="${user.id}">编辑</button> | <button class="btn btn-danger delete-btn" data-id="${user.id}">删除</button> </td> </tr> ` }); userList.innerHTML = output; } // 拉取用户列表 const loadUserList = () => { fetch(baseUrl) .then(res => { if (!res.ok) throw new Error('获取用户列表失败'); return res.json(); }) .then(data => renderUsers(data)) .catch(err => console.error(err)); } // 页面加载完成先拉一次用户列表 loadUserList(); // 提交创建用户 addUserForm.addEventListener('submit', (e) => { e.preventDefault(); // 提交时再读取输入框的值,不要页面加载时就读 const submitData = { user: { name: document.querySelector('#name').value.trim(), email: document.querySelector('#email').value.trim(), password: document.querySelector('#password').value, password_confirmation: document.querySelector('#password-confirmation').value } }; fetch(baseUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(submitData) }) .then(res => { const contentType = res.headers.get('content-type'); // 先判断响应状态,再判断是不是JSON格式,避免解析空响应报错 if (!res.ok) { if (contentType?.includes('application/json')) { return res.json().then(err => Promise.reject(err)); } return Promise.reject({ errors: [`请求失败,状态码:${res.status}`] }); } if (contentType?.includes('application/json')) { return res.json(); } return Promise.reject({ errors: ['接口返回格式不是合法JSON'] }); }) .then(() => { // 创建成功重置表单,重新拉取列表 addUserForm.reset(); loadUserList(); }) .catch(err => { console.error('创建用户失败:', err); alert(err.errors?.join('\n') || '创建用户失败,请检查控制台信息'); }); })
改完之后重启Rails服务,先在Postman里测POST接口确认能正常返回JSON,再测前端提交就不会再报JSON解析错误了。后续写编辑、删除功能的时候,同样要记得先校验响应状态和格式再解析JSON,请求参数按后端要求包裹在user根键下即可。
内容的提问来源于stack exchange,提问作者ejoka
相关产品推荐
相关产品推荐

