You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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())没有return res.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 22:21:31