ReactJS组件出现405(方法不允许)错误

3
我是一名有用的助手,可以为您翻译以下内容。这段代码是尝试使用POST方法调用一个端点。
代码如下:
import React, { Component } from 'react';
import { Input} from 'antd';
import Form from '../../components/uielements/form';
import Button from '../../components/uielements/button';
import Notification from '../../components/notification';
import { adalApiFetch } from '../../adalConfig';


const FormItem = Form.Item;

class CreateSiteCollectionForm extends Component {
    constructor(props) {
        super(props);
        this.state = {Alias:'',DisplayName:'', Description:''};
        this.handleChangeAlias = this.handleChangeAlias.bind(this);
        this.handleChangeDisplayName = this.handleChangeDisplayName.bind(this);
        this.handleChangeDescription = this.handleChangeDescription.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    };

    handleChangeAlias(event){
        this.setState({Alias: event.target.value});
    }

    handleChangeDisplayName(event){
        this.setState({DisplayName: event.target.value});
    }

    handleChangeDescription(event){
        this.setState({Description: event.target.value});
    }

    handleSubmit(e){
        e.preventDefault();
        this.props.form.validateFieldsAndScroll((err, values) => {
            if (!err) {
                let data = new FormData();
                //Append files to form data
                //data.append(

                const options = {
                  method: 'post',
                  body: JSON.stringify(
                    {
                        "Alias": this.state.Alias,
                        "DisplayName": this.state.DisplayName, 
                        "Description": this.state.Description
                    }),
                  config: {
                    headers: {
                      'Content-Type': 'multipart/form-data'
                    }
                  }
                };

                adalApiFetch(fetch, "/SiteCollections/CreateModernSite", options)
                  .then(response =>{
                    if(response.status === 204){
                        Notification(
                            'success',
                            'Site collection created',
                            ''
                            );
                     }else{
                        throw "error";
                     }
                  })
                  .catch(error => {
                    Notification(
                        'error',
                        'Site collection not created',
                        error
                        );
                    console.error(error);
                });
            }
        });      
    }

    render() {
        const { getFieldDecorator } = this.props.form;
        const formItemLayout = {
        labelCol: {
            xs: { span: 24 },
            sm: { span: 6 },
        },
        wrapperCol: {
            xs: { span: 24 },
            sm: { span: 14 },
        },
        };
        const tailFormItemLayout = {
        wrapperCol: {
            xs: {
            span: 24,
            offset: 0,
            },
            sm: {
            span: 14,
            offset: 6,
            },
        },
        };
        return (
            <Form onSubmit={this.handleSubmit}>
                <FormItem {...formItemLayout} label="Alias" hasFeedback>
                {getFieldDecorator('Alias', {
                    rules: [
                        {
                            required: true,
                            message: 'Please input your alias',
                        }
                    ]
                })(<Input name="alias" id="alias" onChange={this.handleChangeAlias} />)}
                </FormItem>
                <FormItem {...formItemLayout} label="Display Name" hasFeedback>
                {getFieldDecorator('displayname', {
                    rules: [
                        {
                            required: true,
                            message: 'Please input your display name',
                        }
                    ]
                })(<Input name="displayname" id="displayname" onChange={this.handleChangedisplayname} />)}
                </FormItem>
                <FormItem {...formItemLayout} label="Description" hasFeedback>
                {getFieldDecorator('description', {
                    rules: [
                        {
                            required: true,
                            message: 'Please input your description',
                        }
                    ],
                })(<Input name="description" id="description"  onChange={this.handleChangeDescription} />)}
                </FormItem>

                <FormItem {...tailFormItemLayout}>
                    <Button type="primary" htmlType="submit">
                        Create modern site
                    </Button>
                </FormItem>
            </Form>
        );
    }
}

const WrappedCreateSiteCollectionForm = Form.create()(CreateSiteCollectionForm);
export default WrappedCreateSiteCollectionForm;

而WebAPI是这个:

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Http;
using TenantManagementWebApi.Entities;
using TenantManagementWebApi.Factories;
using Cosmonaut.Extensions;
using Microsoft.Online.SharePoint.TenantAdministration;
using Microsoft.SharePoint.Client;
using OfficeDevPnP.Core.Sites;
using TenantManagementWebApi.Components;

namespace TenantManagementWebApi.Controllers
{
    [Authorize]
    public class SiteCollectionsController : ApiController
    {
        // GET: ModernTeamSite
        public async Task<List<TenantManagementWebApi.Entities.SiteCollection>> Get()
        {
            var tenant = await TenantHelper.GetTenantAsync();

            using (var cc = new OfficeDevPnP.Core.AuthenticationManager().GetAppOnlyAuthenticatedContext(tenant.TenantAdminUrl, tenant.ClientId, tenant.ClientSecret))
            {
                Tenant tenantOnline = new Tenant(cc);
                SPOSitePropertiesEnumerable siteProps = tenantOnline.GetSitePropertiesFromSharePoint("0", true);
                cc.Load(siteProps);
                cc.ExecuteQuery();
                List<TenantManagementWebApi.Entities.SiteCollection> sites = new List<TenantManagementWebApi.Entities.SiteCollection>();
                foreach (var site in siteProps)
                {

                    sites.Add(new TenantManagementWebApi.Entities.SiteCollection()
                    {
                        Url = site.Url,
                        Owner = site.Owner,
                        Template = site.Template,
                        Title = site.Title
                    });
                }

                return sites;
            };
        }

        [HttpPost]
        //[Route("api/SiteCollections/CreateModernSite")]
        public async Task<string>  CreateModernSite(string Alias, string DisplayName, string Description)
        {
            var tenant = await TenantHelper.GetTenantAsync();
            using (var context = new OfficeDevPnP.Core.AuthenticationManager().GetAppOnlyAuthenticatedContext(tenant.TenantAdminUrl, tenant.ClientId, tenant.ClientSecret))
            {
                 var teamContext = await context.CreateSiteAsync(
                    new TeamSiteCollectionCreationInformation
                    {
                        Alias = Alias, // Mandatory
                        DisplayName = DisplayName, // Mandatory
                        Description = Description, // Optional
                        //Classification = Classification, // Optional
                        //IsPublic = IsPublic, // Optional, default true
                    }
                );
                teamContext.Load(teamContext.Web, w => w.Url);
                teamContext.ExecuteQueryRetry();
                return teamContext.Web.Url;
            }
        }
    }
}

将“Content-Type”:”multipart/form-data”更改为“application/x-www-form-urlencoded” - Nisfan
HTTP POST已经被使用了,我还在选项头中放置了POST。也许我漏掉了application./json。 - Luis Valencia
在将 application/json 更改后,我仍然收到“方法不允许”的错误。 - Luis Valencia
我将路由取消注释了,但是没有任何区别。 - Luis Valencia
请提供文本内容,不要发送链接。我将为您翻译相关的编程文字。 - Luis Valencia
是的,返回已翻译的文本: https://www.screencast.com/t/WAYBsqy09 - Luis Valencia
2个回答

2
根据评论中发布的截图显示,属性路由已启用,因为WebApiConfig具有默认配置。
public static class WebApiConfig {
    public static void Register(HttpConfiguration config) {
        // Attribute routing.
        config.MapHttpAttributeRoutes();

        // Convention-based routing.
        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
    }
}

请注意约定路由上的api前缀。
客户端发出的请求被发送到/SiteCollections/CreateModernSite,这不会匹配Web API,因为API控制器似乎没有使用属性路由,并且请求的URL与Web API约定路由不匹配。
此外,在客户端端,JSON正文是在选项中构建的,而内容类型设置为'multipart/form-data'
如果意图是在正文中发布内容,则在服务器端,您需要进行一些更改以使API可访问。
[Authorize]
[RoutePrefix("api/SiteCollections")]
public class SiteCollectionsController : ApiController {
    // GET api/SiteCollections
    [HttpGet]
    [Route("")]
    public async Task<IHttpActionResult> Get() {
        var tenant = await TenantHelper.GetTenantAsync();
        using (var cc = new OfficeDevPnP.Core.AuthenticationManager().GetAppOnlyAuthenticatedContext(tenant.TenantAdminUrl, tenant.ClientId, tenant.ClientSecret)) {
            var tenantOnline = new Tenant(cc);
            SPOSitePropertiesEnumerable siteProps = tenantOnline.GetSitePropertiesFromSharePoint("0", true);
            cc.Load(siteProps);
            cc.ExecuteQuery();
            var sites = siteProps.Select(site => 
                new TenantManagementWebApi.Entities.SiteCollection() {
                    Url = site.Url,
                    Owner = site.Owner,
                    Template = site.Template,
                    Title = site.Title
                })
                .ToList();
            return Ok(sites);
        }
    }

    // POST api/SiteCollections
    [HttpPost]
    [Route("")]
    public async Task<IHttpActionResult>  CreateModernSite([FromBody]NewSiteInformation model) {
        if(ModelState.IsValid) {
            var tenant = await TenantHelper.GetTenantAsync();
            using (var context = new OfficeDevPnP.Core.AuthenticationManager().GetAppOnlyAuthenticatedContext(tenant.TenantAdminUrl, tenant.ClientId, tenant.ClientSecret)) {
                 var teamContext = await context.CreateSiteAsync(
                    new TeamSiteCollectionCreationInformation {
                        Alias = model.Alias, // Mandatory
                        DisplayName = model.DisplayName, // Mandatory
                        Description = model.Description, // Optional
                        //Classification = Classification, // Optional
                        //IsPublic = IsPublic, // Optional, default true
                    }
                );
                teamContext.Load(teamContext.Web, _ => _.Url);
                teamContext.ExecuteQueryRetry();
                //204 with location and content set to created URL
                return Created(teamContext.Web.Url, teamContext.Web.Url);
            }
        }
        return BadRequest(ModelState);
    }

    public class NewSiteInformation {
        [Required]
        public string Alias { get; set; }
        [Required]
        public string DisplayName { get; set; }
        public string Description { get; set; }
        //...
    }
}

请注意,POST操作中包含了适当的强类型对象模型、模型验证以及按照客户端期望的HTTP状态码返回结果。(204)
在客户端上,更新调用的URL以匹配API控制器的路由,并更新选项以发送正确的内容类型。
//...

const options = {
  method: 'POST',
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(
    {
        Alias: this.state.Alias,
        DisplayName: this.state.DisplayName, 
        Description: this.state.Description
    })      
};

adalApiFetch(fetch, "api/SiteCollections", options)
  .then(response =>{
    if(response.status === 204){
        Notification(
            'success',
            'Site collection created',
            ''
            );
     }else{
        throw "error";
     }
  })
  .catch(error => {
    Notification(
        'error',
        'Site collection not created',
        error
        );
    console.error(error);
});

//...

请注意,与原始代码中的config.headers不同,headers直接在获取选项中。

谢谢,非常详细的解释。我按照你说的做了,但我还是遇到了这个问题:https://screencast.com/t/txuDXlEmtBL - Luis Valencia
@LuisValencia 你能看到原始请求以确认请求的内容类型吗?我觉得很奇怪它不接受 JSON,因为这是 asp.net-web-api 的默认设置。 - Nkosi
这很奇怪,如果我使用Postman进行操作,它可以正常工作,但是如果我在React应用程序中进行操作,则内容类型为text/plain。 - Luis Valencia
@LuisValencia 检查更新。我认为这与您在获取选项中设置标头的方式有关。在将有效载荷字符串化之前,我还必须更新其格式。 - Nkosi

1

尝试将您的Post请求参数移入一个类中,如下所示

public class TeamSiteInformation
{
    public string Alias { get; set; }
    public string DisplayName  { get; set; }
    public string Description   { get; set; }
}

并修改您的CreateModernSite方法签名为
[HttpPost]
public void CreateModernSite([FromBody]TeamSiteInformation site_info)
{

在您的ReactJS应用程序中,将“Content-Type”从“multipart/form-data”更改为“application/json”


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接